17

の形式の時系列があり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;
        }
    }
}
4

8 に答える 8

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;

累積値(匿名タイプとして実装)には、2つのフィールドResultが含まれます。これまでに作成された結果リストが含まれます。Working最後のperiod-1要素が含まれています。集計関数は、現在の値を作業リストに追加し、現在の平均を作成して結果に追加してから、最初の(つまり最も古い)値を作業リストから削除します。

「シード」(つまり、累積の開始値)は、最初のperiod-1要素を空のリストに入れてWorking初期化することによって構築されます。Result

したがって、集計は要素から始まりますperiod(最初の要素をスキップすることにより(period-1)

関数型プログラミングでは、これはaggretate(またはfold)関数の典型的な使用パターンです。

2つの意見:

Working同じリストオブジェクト(およびResult)がすべてのステップで再利用されるという点で、ソリューションは「機能的に」クリーンではありません。将来のコンパイラがAggregate関数を自動的に並列化しようとした場合に問題が発生する可能性があるかどうかはわかりません(一方、それが可能かどうかもわかりません...)。純粋に機能的なソリューションは、すべてのステップで新しいリストを「作成」する必要があります。

また、C#には強力なリスト式がないことに注意してください。いくつかの架空のPython-C#混合擬似コードでは、次のような集計関数を記述できます。

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

これは私の謙虚な意見ではもう少しエレガントでしょう:)

于 2011-03-03T00:48:41.863 に答える
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; } }
}

このクラスは、MovingAverageフィルターの非常に高速で軽量な実装を提供します。これは、長さNの循環バッファを作成し、ブルートフォース実装のポイントごとにNの乗算加算とは対照的に、追加されたデータポイントごとに1つの加算、1つの減算、および1つの乗算を計算します。

次に、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;
        }
    }
}

上記の拡張メソッドは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);
于 2014-04-14T22:01:47.370 に答える
7

LINQの使用方法を示す回答はすでにあります、率直に言って、現在のソリューションと比較してパフォーマンスが低く、既存のコードがすでに明確であるため、ここではLINQを使用しません。

ただし、periodすべてのステップで前の要素の合計を計算する代わりに、現在の合計を維持し、各反復で調整することができます。つまり、これを変更します。

total = 0;
for (int x = i; x > (i - period); x--)
    total += series.Values[x];

これに:

if (i >= period) {
    total -= series.Values[i - period];
}
total += series.Values[i];

これは、のサイズに関係なく、コードの実行に同じ時間がかかることを意味しますperiod

于 2011-03-02T11:23:09.387 に答える
7

このブロック

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

次のように書き直すことができます。

double average = series.Values.Skip(i - period + 1).Take(period).Sum() / period;

メソッドは次のようになります。

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

ご覧のとおり、linqは非常に表現力豊かです。LINQや101LINQサンプルの紹介などのチュートリアルから始めることをお勧めします。

于 2011-03-02T13:14:47.160 に答える
3

これをより機能的な方法で行うにScanは、Rxには存在するが、LINQには存在しないメソッドが必要です。

スキャンメソッドがあるとしたらどうなるか見てみましょう

var delta = 3;
var series = new [] {1.1, 2.5, 3.8, 4.8, 5.9, 6.1, 7.6};

var seed = series.Take(delta).Average();
var smas = series
    .Skip(delta)
    .Zip(series, Tuple.Create)
    .Scan(seed, (sma, values)=>sma - (values.Item2/delta) + (values.Item1/delta));
smas = Enumerable.Repeat(0.0, delta-1).Concat(new[]{seed}).Concat(smas);

そして、ここから取得して調整したスキャン方法を次に示します

public static IEnumerable<TAccumulate> Scan<TSource, TAccumulate>(
    this IEnumerable<TSource> source,
    TAccumulate seed,
    Func<TAccumulate, TSource, TAccumulate> accumulator
)
{
    if (source == null) throw new ArgumentNullException("source");
    if (seed == null) throw new ArgumentNullException("seed");
    if (accumulator == null) throw new ArgumentNullException("accumulator");

    using (var i = source.GetEnumerator())
    {
        if (!i.MoveNext())
        {
            throw new InvalidOperationException("Sequence contains no elements");
        }
        var acc = accumulator(seed, i.Current);

        while (i.MoveNext())
        {
            yield return acc;
            acc = accumulator(acc, i.Current);
        }
        yield return acc;
    }
}

SMAの計算に現在の合計を使用しているため、これはブルートフォース方式よりも優れたパフォーマンスを発揮するはずです。

何が起きてる?

まず、seedここで呼び出す最初の期間を計算する必要があります。次に、累積シード値から計算する後続のすべての値。そのためには、古い値(つまり、t-delta)と、シリーズを最初から1回、デルタによってシフトした最新の値が必要です。

最後に、最初の期間の長さにゼロを追加し、初期シード値を追加することによって、いくつかのクリーンアップを実行します。

于 2013-06-19T22:58:08.553 に答える
2

もう1つのオプションは、MoreLINQWindowedメソッドを使用することです。これにより、コードが大幅に簡素化されます。

var averaged = mySeries.Windowed(period).Select(window => window.Average(keyValuePair => keyValuePair.Value));
于 2017-10-10T06:32:15.193 に答える
0

このコードを使用してSMAを計算します。

private void calculateSimpleMA(decimal[] values, out decimal[] buffer)
{
    int period = values.Count();     // gets Period (assuming Period=Values-Array-Size)
    buffer = new decimal[period];    // initializes buffer array
    var sma = SMA(period);           // gets SMA function
    for (int i = 0; i < period; i++)
        buffer[i] = sma(values[i]);  // fills buffer with SMA calculation
}

static Func<decimal, decimal> SMA(int p)
{
    Queue<decimal> s = new Queue<decimal>(p);
    return (x) =>
    {
        if (s.Count >= p)
        {
            s.Dequeue();
        }
        s.Enqueue(x);
        return s.Average();
    };
}
于 2015-04-01T15:02:29.577 に答える
0

拡張メソッドは次のとおりです。

public static IEnumerable<double> MovingAverage(this IEnumerable<double> source, int period)
{
    if (source is null)
    {
        throw new ArgumentNullException(nameof(source));
    }

    if (period < 1)
    {
        throw new ArgumentOutOfRangeException(nameof(period));
    }

    return Core();

    IEnumerable<double> Core()
    {
        var sum = 0.0;
        var buffer = new double[period];
        var n = 0;
        foreach (var x in source)
        {
            n++;
            sum += x;
            var index = n % period;
            if (n >= period)
            {
                sum -= buffer[index];
                yield return sum / period;
            }

            buffer[index] = x;
        }
    }
}
于 2021-05-24T09:18:32.880 に答える