【问题标题】:Calculate average difference between a series of DateTime objects using LINQ使用 LINQ 计算一系列 DateTime 对象之间的平均差异
【发布时间】:2021-09-29 01:53:41
【问题描述】:

给定一个IEnumerable<DateTime>,有没有办法使用LINQ 计算DateTime 对象的后续元素之间差异的平均值?

编辑:我意识到我原来的措辞有点不清楚。澄清一下,我希望平均元素 0 和 1、1 和 2、2 和 3 等之间的差异。

double GetAvgDiffInMilliseconds(IEnumerable<DateTime> items)
   => items.Aggregate(/* ??? */).Average();  // is this possible with LINQ?

注意:我对非 LINQ 解决方案不感兴趣,因为这个问题可以通过循环和累加器轻松解决。出于学习目的,我想了解 LINQ 是否可以做到这一点。

【问题讨论】:

  • LINQ 操作使用迭代器和 IEnumerable。你问的是一个使用当前和上一个项目的运算符,例如MoreLINQ's Pairwise 运算符。其他任何东西都会贵很多
  • 要让Aggregate 工作,您必须传输一个值,例如一个元组,其中包含以前的日期、计数和差的总和作为累加器。 Aggregate 的主体必须增加计数,计算前一个值和当前值之间的差异,并发出一个新的元组,其中包含增加的计数、当前值和更新的差异总和。最后一步必须将总和除以计数。这非常丑陋而坦率地说,不值得麻烦
  • 你的意思是所有可能组合与这些元素的平均距离,还是只是 N N + 1
  • @CSharpie 我的意思是每个后续条目之间差异的平均值,因此 item1 - item0、item2 - item1 等。
  • @CSharpie 根据您的评论,我澄清了我的问题,任何人可能会在未来发现这篇文章有用。谢谢!

标签: c# linq


【解决方案1】:

如果集合中有多个元素,您可以使用Zip

items.Zip(items.Skip(1))
    .Select(tuple => (tuple.Second - tuple.First).TotalMilliseconds)
    .Average();

【讨论】:

  • 这对于IList&lt;DateTime&gt; 类型的源来说会很好,但应不惜一切代价避免多次枚举未知来源的IEnumerable&lt;T&gt;s。
【解决方案2】:

LINQ 没有这样的运算符yet,但 LINQ-to-Objects 操作无论如何都使用迭代器、枚举器和 IEnumerable。你问的是一个使用当前和上一个项目的运算符,例如MoreLINQ's Pairwise 运算符。这样的操作只需要一次迭代来处理对并产生输出。其他任何东西都会贵很多。

使用Pairwise你可以写:

var avg=items.Pairwise((a, b) => (a - b))
             .Average(ts=>ts.TotalMilliseconds);

Pairwise的代码很简单,不需要多次迭代:

    public static IEnumerable<TResult> Pairwise<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TSource, TResult> resultSelector)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector));

        return _(); IEnumerable<TResult> _()
        {
            using var e = source.GetEnumerator();

            if (!e.MoveNext())
                yield break;

            var previous = e.Current;
            while (e.MoveNext())
            {
                yield return resultSelector(previous, e.Current);
                previous = e.Current;
            }
        }
    }

Pairwise 方法可能会在某个时候出现在 LINQ 中。这是函数式语言中非常常见的操作,.NET Core 6 Preview 4 中添加了一些MoreLINQ 操作符,例如DistinctByMaxBy 等等。

使用聚合

可以使用Aggregate 来计算Average,但它非常丑陋且浪费。

如果你有一个数字列表,你需要携带累加器中的项目计数和项目总和,并计算最后的平均值:

    var nums=new[]{1.0,2.0,3.0};
    var avg=nums.Aggregate(
        (cnt:0.0,sum:0.0),
        (acc,c)=>(cnt:acc.cnt+1,sum:acc.sum+c),
        acc=>acc.sum/acc.cnt);
    Console.WriteLine(avg);

在这种情况下,虽然您想计算当前项目与前一个项目之间的差异。这意味着您需要携带上一个值,差的总和,并将当前值和上一个值之间的差相加。

当没有前一个值时,您还需要处理第一个值。由于您计算差异,因此实际计数比集合的计数少一。最后,如果只有两项不能分:

    var nums=new[]{DateTime.Now,DateTime.Now.AddMinutes(1),DateTime.Now.AddMinutes(2)};
    var avg=nums.Aggregate(
        (cnt:0.0,sum:0.0,prev:DateTime.MinValue),
        (acc,c)=>( cnt:acc.cnt+1,
                   sum:(acc.prev==DateTime.MinValue)
                       ?0
                       :acc.sum+(c-acc.prev).TotalMilliseconds,
                   prev:c),
        acc=>acc.cnt==1
             ?acc.sum
             :acc.sum/(acc.cnt-1));
    Console.WriteLine(avg);

这会打印出60002.95775。它还需要大量的反复试验和 NaN 才能使其工作。

【讨论】:

  • 很全面的回答,谢谢! Pairwise 正是我想要的,但它也有助于理解为什么 Aggregate 解决方案效率很低。再次感谢您抽出宝贵时间来编写它!
  • @CoolBots 如果你看看像 F# 这样的函数式语言,你会发现它们有 fold,相当于 Aggregate,但也有优化的成对方法。仅仅因为某事是可能的并且在逻辑上等效并不意味着这是一个好主意
猜你喜欢
  • 1970-01-01
  • 2022-10-23
  • 2022-08-21
  • 2021-02-17
  • 1970-01-01
  • 2013-06-11
  • 1970-01-01
  • 2018-05-23
  • 1970-01-01
相关资源
最近更新 更多