LINQ计算SortedList的移动平均值<dateTime,double>

And*_* P. 15 c# linq moving-average

我有一个时间序列的形式SortedList<dateTime,double>.我想计算一下这个系列的移动平均线.我可以使用简单的for循环来做到这一点.我想知道是否有更好的方法来使用linq.

我的版本:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var mySeries = new SortedList<DateTime, double>();
            mySeries.Add(new DateTime(2011, 01, 1), 10);
            mySeries.Add(new DateTime(2011, 01, 2), 25);
            mySeries.Add(new DateTime(2011, 01, 3), 30);
            mySeries.Add(new DateTime(2011, 01, 4), 45);
            mySeries.Add(new DateTime(2011, 01, 5), 50);
            mySeries.Add(new DateTime(2011, 01, 6), 65);

            var calcs = new calculations();
            var avg = calcs.MovingAverage(mySeries, 3);
            foreach (var item in avg)
            {
                Console.WriteLine("{0} {1}", item.Key, item.Value);                
            }
        }
    }
    class calculations
    {
        public SortedList<DateTime, double> MovingAverage(SortedList<DateTime, double> series, int period)
        {
            var result = new SortedList<DateTime, double>();

            for (int i = 0; i < series.Count(); i++)
            {
                if (i >= period - 1)
                {
                    double total = 0;
                    for (int x = i; x > (i - period); x--)
                        total += series.Values[x];
                    double average = total / period;
                    result.Add(series.Keys[i], average);  
                }

            }
            return result;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Mar*_*ner 19

为了实现O(n)的渐近性能(如手工编码解决方案所做的那样),你可以使用Aggregate

series.Skip(period-1).Aggregate(
  new {
    Result = new SortedList<DateTime, double>(), 
    Working = List<double>(series.Take(period-1).Select(item => item.Value))
  }, 
  (list, item)=>{
     list.Working.Add(item.Value); 
     list.Result.Add(item.Key, list.Working.Average()); 
     list.Working.RemoveAt(0);
     return list;
  }
).Result;
Run Code Online (Sandbox Code Playgroud)

累积值(以匿名类型实现)包含两个字段:Result包含到目前为止构建的结果列表.Working包含最后的period-1元素.聚合函数将当前值添加到工作列表,构建当前平均值并将其添加到结果中,然后从工作列表中删除第一个(即最旧的)值.

"种子"(即累积的起始值)是通过将第一个period-1元素放入Working并初始化Result为空列表来构建的.

因此,聚合从元素开始period(通过跳过(period-1)元素开头)

在函数式编程中,这是aggretate(或fold)函数的典型使用模式,顺便说一句.

两个评论:

该解决方案不是"功能上"干净的,因为在每个步骤中都重复使用相同的列表对象(WorkingResult).如果未来的某些编译器试图自动并行化Aggregate函数,我不确定这是否会引起问题(另一方面,我也不确定,如果可能的话......).纯功能解决方案应该在每一步"创建"新列表.

另请注意,C#缺少强大的列表表达式.在一些假设的Python-C#混合伪代码中,可以编写聚合函数

(list, item)=>
  new {
    Result = list.Result + [(item.Key, (list.Working+[item.Value]).Average())], 
    Working=list.Working[1::]+[item.Value]
  }
Run Code Online (Sandbox Code Playgroud)

在我的拙见:)这会更优雅:)


Dr.*_*ABT 12

为了使用LINQ计算移动平均线的最有效方法,您不应该使用LINQ!

相反,我建议创建一个辅助类,以尽可能最有效的方式计算移动平均值(使用循环缓冲区和因果移动平均滤波器),然后使用扩展方法使LINQ可以访问它.

首先是移动平均线

public class MovingAverage
{
    private readonly int _length;
    private int _circIndex = -1;
    private bool _filled;
    private double _current = double.NaN;
    private readonly double _oneOverLength;
    private readonly double[] _circularBuffer;
    private double _total;

    public MovingAverage(int length)
    {
        _length = length;
        _oneOverLength = 1.0 / length;
        _circularBuffer = new double[length];
    }       

    public MovingAverage Update(double value)
    {
        double lostValue = _circularBuffer[_circIndex];
        _circularBuffer[_circIndex] = value;

        // Maintain totals for Push function
        _total += value;
        _total -= lostValue;

        // If not yet filled, just return. Current value should be double.NaN
        if (!_filled)
        {
            _current = double.NaN;
            return this;
        }

        // Compute the average
        double average = 0.0;
        for (int i = 0; i < _circularBuffer.Length; i++)
        {
            average += _circularBuffer[i];
        }

        _current = average * _oneOverLength;

        return this;
    }

    public MovingAverage Push(double value)
    {
        // Apply the circular buffer
        if (++_circIndex == _length)
        {
            _circIndex = 0;
        }

        double lostValue = _circularBuffer[_circIndex];
        _circularBuffer[_circIndex] = value;

        // Compute the average
        _total += value;
        _total -= lostValue;

        // If not yet filled, just return. Current value should be double.NaN
        if (!_filled && _circIndex != _length - 1)
        {
            _current = double.NaN;
            return this;
        }
        else
        {
            // Set a flag to indicate this is the first time the buffer has been filled
            _filled = true;
        }

        _current = _total * _oneOverLength;

        return this;
    }

    public int Length { get { return _length; } }
    public double Current { get { return _current; } }
}
Run Code Online (Sandbox Code Playgroud)

此类提供了一个非常快速且轻量级的MovingAverage过滤器实现.它创建一个长度为N的循环缓冲区,并计算每附加一个数据点的一个加法,一个减法和一个乘法,而不是强力实现的每个点的N乘加.

接下来,到LINQ-ify吧!

internal static class MovingAverageExtensions
{
    public static IEnumerable<double> MovingAverage<T>(this IEnumerable<T> inputStream, Func<T, double> selector, int period)
    {
        var ma = new MovingAverage(period);
        foreach (var item in inputStream)
        {
            ma.Push(selector(item));
            yield return ma.Current;
        }
    }

    public static IEnumerable<double> MovingAverage(this IEnumerable<double> inputStream, int period)
    {
        var ma = new MovingAverage(period);
        foreach (var item in inputStream)
        {
            ma.Push(item);
            yield return ma.Current;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

上述扩展方法包装MovingAverage类并允许插入IEnumerable流.

现在用它!

int period = 50;

// Simply filtering a list of doubles
IEnumerable<double> inputDoubles;
IEnumerable<double> outputDoubles = inputDoubles.MovingAverage(period);   

// Or, use a selector to filter T into a list of doubles
IEnumerable<Point> inputPoints; // assuming you have initialised this
IEnumerable<double> smoothedYValues = inputPoints.MovingAverage(pt => pt.Y, period);
Run Code Online (Sandbox Code Playgroud)

  • 很棒的方法! (2认同)

Mar*_*ers 7

你已经有一个展示你如何回答可以使用LINQ,但坦白说,我不会使用LINQ在这里,因为它会比较差的最有可能执行到您当前的解决方案和现有的代码已经是明确的.

但是,不是计算period每个步骤中前面元素的总和,而是可以保持运行总计并在每次迭代时调整它.也就是说,改变这个:

total = 0;
for (int x = i; x > (i - period); x--)
    total += series.Values[x];
Run Code Online (Sandbox Code Playgroud)

对此:

if (i >= period) {
    total -= series.Values[i - period];
}
total += series.Values[i];
Run Code Online (Sandbox Code Playgroud)

这意味着无论大小如何,您的代码都将花费相同的时间来执行period.

  • 在我看来,不要使用LINQ是问题的有效答案.LINQ很精彩,但这是错误的工具. (3认同)

Bra*_*mir 7

这个块

double total = 0;
for (int x = i; x > (i - period); x--)
    total += series.Values[x];
double average = total / period;
Run Code Online (Sandbox Code Playgroud)

可以改写为:

double average = series.Values.Skip(i - period + 1).Take(period).Sum() / period;
Run Code Online (Sandbox Code Playgroud)

您的方法可能如下所示:

series.Skip(period - 1)
    .Select((item, index) =>
        new 
        {
            item.Key,            
            series.Values.Skip(index).Take(period).Sum() / period
        });
Run Code Online (Sandbox Code Playgroud)

如你所见,linq非常富有表现力.我建议从一些教程开始,比如介绍LINQ101 LINQ Samples.

  • 注意*O(n ^ 2)*的运行时间,因为你需要在每一步跳过越来越多的元素(并且afaik`Skip(i)`必须调用`IEnumerator.MoveNext`*i*次).请看我在*O(n)*时间内的解决方案的响应...(我刚刚注意到下面的OP评论,他/她将来可能会从SQL DB中获取值.在这种情况下,我会强烈劝阻从这个解决方案!) (2认同)