web-dev-qa-db-ja.com

LINQは期間ごとに集計およびグループ化します

LINQを使用して時間間隔でデータをグループ化する方法を理解しようとしています。そして理想的には各グループを集約します。

日付範囲が明示された多数の例を見つけて、5分、1時間、1日などの期間でグループ化しようとしています。

たとえば、DateTimeを値でラップするクラスがあります。

public class Sample
{
     public DateTime timestamp;
     public double value;
}

これらの観測は、Listコレクションのシリーズとして含まれています。

List<Sample> series;

したがって、1時間ごとにグループ化し、平均値を集計するには、次のようにします。

var grouped = from s in series
              group s by new TimeSpan(1, 0, 0) into g
              select new { timestamp = g.Key, value = g.Average(s => s.value };

TimeSpan自体をグループ化するため、これには根本的な欠陥があります。クエリでTimeSpan(または間隔を表すデータ型)を使用する方法を理解できません。

34
Jason Sturges

タイムスタンプを次の境界に丸めて(つまり、過去の最も近い5分の境界に切り捨てて)、それをグループ化として使用できます。

var groups = series.GroupBy(x =>
{
    var stamp = x.timestamp;
    stamp = stamp.AddMinutes(-(stamp.Minute % 5));
    stamp = stamp.AddMilliseconds(-stamp.Millisecond - 1000 * stamp.Second);
    return stamp;
})
.Select(g => new { TimeStamp = g.Key, Value = g.Average(s => s.value) })
.ToList();

上記では、グループ化で変更されたタイムスタンプを使用して、分を前の5分の境界に設定し、秒とミリ秒を削除することでそれを実現しています。もちろん、同じアプローチを他の期間、つまり時間と日にも使用できます。

編集:

これに基づいて作成されたサンプル入力:

var series = new List<Sample>();
series.Add(new Sample() { timestamp = DateTime.Now.AddMinutes(3) });
series.Add(new Sample() { timestamp = DateTime.Now.AddMinutes(4) });
series.Add(new Sample() { timestamp = DateTime.Now.AddMinutes(5) });
series.Add(new Sample() { timestamp = DateTime.Now.AddMinutes(6) });
series.Add(new Sample() { timestamp = DateTime.Now.AddMinutes(7) });
series.Add(new Sample() { timestamp = DateTime.Now.AddMinutes(15) });

3つのグループが作成されました。1つはグループ化タイムスタンプ3:05、もう1つは午後3時10分、午後3時20分です(結果は現在の時刻によって異なる場合があります)。

45
BrokenGlass

タイムスタンプを丸める関数が必要です。何かのようなもの:

 var grouped = from s in series
          group s by new DateTime(s.timestamp.Year, s.timestamp.Month,  
                s.timestamp.Day, s.timestamp.Hour, 0, 0) into g
          select new { timestamp = g.Key, value = g.Average(s => s.value };

1時間ごとのビンの場合。また、結果のタイムスタンプは、TimeSpanではなく、DateTimeになることに注意してください。

11
Henk Holterman

私はこれにゲームに非常に遅れていますが、何か他のものを探している間にこれに遭遇しました、そして私はもっと良い方法があると思いました。

series.GroupBy (s => s.timestamp.Ticks / TimeSpan.FromHours(1).Ticks)
        .Select (s => new {
            series = s
            ,timestamp = s.First ().timestamp
            ,average = s.Average (x => x.value )
        }).Dump();

これはサンプルのlinqpadプログラムですので、検証およびテストできます

void Main()
{
    List<Sample> series = new List<Sample>();

    Random random = new Random(DateTime.Now.Millisecond);
    for (DateTime i = DateTime.Now.AddDays(-5); i < DateTime.Now; i += TimeSpan.FromMinutes(1))
    {
        series.Add(new UserQuery.Sample(){ timestamp = i, value = random.NextDouble() * 100 });
    }
    //series.Dump();
    series.GroupBy (s => s.timestamp.Ticks / TimeSpan.FromHours(1).Ticks)
        .Select (s => new {
            series = s
            ,timestamp = s.First ().timestamp
            ,average = s.Average (x => x.value )
        }).Dump();
}

// Define other methods and classes here
public class Sample
{
     public DateTime timestamp;
     public double value;
}
6
Duane McKinney

時間でグループ化するには、タイムスタンプの時間の部分でグループ化する必要があります。

var groups = from s in series
  let groupKey = new DateTime(s.timestamp.Year, s.timestamp.Month, s.timestamp.Day, s.timestamp.Hour, 0, 0)
  group s by groupKey into g select new
                                      {
                                        TimeStamp = g.Key,
                                        Value = g.Average(a=>a.value)
                                      };
2
Michael

new DateTime()toavoidanyミリ秒未満の問題

var versionsGroupedByRoundedTimeAndAuthor = db.Versions.GroupBy(g => 
new
{
                UserID = g.Author.ID,
                Time = RoundUp(g.Timestamp, TimeSpan.FromMinutes(2))
});

  private DateTime RoundUp(DateTime dt, TimeSpan d)
        {
            return new DateTime(((dt.Ticks + d.Ticks - 1) / d.Ticks) * d.Ticks);
        }

N.B.ここでは、Author.IDと丸められたTimeStampでグループ化しています。

@dtbの回答から取得したRoundUp関数 https://stackoverflow.com/a/7029464/661584

ミリ秒までの等値性が必ずしも等値性を意味するわけではないので、ここで読んでください DateTimeの等値性をテストするときにこのユニットテストが失敗するのはなぜですか?

1
MemeDeveloper

私はこれで直接質問に答えられないことを知っていますが、私は株式/暗号通貨のキャンドルデータを短い分から高い分(5、10、15、30)まで集計する非常に類似したソリューションを探して探し回っていました。集計された期間のタイムスタンプには一貫性がないため、現在の分から一度にXだけに戻ることはできません。また、リストの最初と最後に、より長い期間のろうそく足全体を埋めるのに十分なデータがあることにも注意する必要があります。そこで、私が思いついた解決策は次のとおりです。 (rawPeriodで示されるように、より短い期間のろうそくは、タイムスタンプの昇順でソートされると想定しています。)

public class Candle
{
    public long Id { get; set; }
    public Period Period { get; set; }
    public DateTime Timestamp { get; set; }
    public double High { get; set; }
    public double Low { get; set; }
    public double Open { get; set; }
    public double Close { get; set; }
    public double BuyVolume { get; set; }
    public double SellVolume { get; set; }
}

public enum Period
{
    Minute = 1,
    FiveMinutes = 5,
    QuarterOfAnHour = 15,
    HalfAnHour = 30
}

    private List<Candle> AggregateCandlesIntoRequestedTimePeriod(Period rawPeriod, Period requestedPeriod, List<Candle> candles)
    {
        if (rawPeriod != requestedPeriod)
        {
            int rawPeriodDivisor = (int) requestedPeriod;
            candles = candles
                        .GroupBy(g => new { TimeBoundary = new DateTime(g.Timestamp.Year, g.Timestamp.Month, g.Timestamp.Day, g.Timestamp.Hour, (g.Timestamp.Minute / rawPeriodDivisor) * rawPeriodDivisor , 0) })
                        .Where(g => g.Count() == rawPeriodDivisor )
                        .Select(s => new Candle
                        {
                            Period = requestedPeriod,
                            Timestamp = s.Key.TimeBoundary,
                            High = s.Max(z => z.High),
                            Low = s.Min(z => z.Low),
                            Open = s.First().Open,
                            Close = s.Last().Close,
                            BuyVolume = s.Sum(z => z.BuyVolume),
                            SellVolume = s.Sum(z => z.SellVolume),
                        })
                        .OrderBy(o => o.Timestamp)
                        .ToList();
        }

        return candles;
    }
0
vipes

BrokenGlassの回答をより一般的なものにし、保護手段を追加することで改善しました。彼の現在の答えでは、9の間隔を選択した場合、期待したとおりに動作しません。 60が割り切れない数についても同様です。この例では、9を使用しており、午前0時(0:00)から開始します。

  • 0:00から0:08.999まではすべて、予想どおり0:00のグループに入れられます。 0:54で始まるグループに到達するまで、これを続けます。
  • 0:54では、01:03.999まで上がるのではなく、0:54から0:59.999までのみをグループ化します。

私にとって、これは大きな問題です。

どのように修正すればよいかわかりませんが、保護手段を追加できます。
変更:

  1. 60%[間隔]が0に等しい任意の分が許容可能な間隔になります。以下のifステートメントはこれを保護します。
  2. 時間間隔も機能します。

            double minIntervalAsDouble = Convert.ToDouble(minInterval);
            if (minIntervalAsDouble <= 0)
            {
                string message = "minInterval must be a positive number, exiting";
                Log.getInstance().Info(message);
                throw new Exception(message);
            }
            else if (minIntervalAsDouble < 60.0 && 60.0 % minIntervalAsDouble != 0)
            {
                string message = "60 must be divisible by minInterval...exiting";
                Log.getInstance().Info(message);
                throw new Exception(message);
            }
            else if (minIntervalAsDouble >= 60.0 && (24.0 % (minIntervalAsDouble / 60.0)) != 0 && (24.0 % (minIntervalAsDouble / 60.0) != 24.0))
            {
                //hour part must be divisible...
                string message = "If minInterval is greater than 60, 24 must be divisible by minInterval/60 (hour value)...exiting";
                Log.getInstance().Info(message);
                throw new Exception(message);
            }
            var groups = datas.GroupBy(x =>
            {
                if (minInterval < 60)
                {
                    var stamp = x.Created;
                    stamp = stamp.AddMinutes(-(stamp.Minute % minInterval));
                    stamp = stamp.AddMilliseconds(-stamp.Millisecond);
                    stamp = stamp.AddSeconds(-stamp.Second);
                    return stamp;
                }
                else
                {
                    var stamp = x.Created;
                    int hourValue = minInterval / 60;
                    stamp = stamp.AddHours(-(stamp.Hour % hourValue));
                    stamp = stamp.AddMilliseconds(-stamp.Millisecond);
                    stamp = stamp.AddSeconds(-stamp.Second);
                    stamp = stamp.AddMinutes(-stamp.Minute);
                    return stamp;
                }
            }).Select(o => new
            {
                o.Key,
                min = o.Min(f=>f.Created),
                max = o.Max(f=>f.Created),
                o
            }).ToList();
    

あなたが好きなものをselect文に入れてください!テストが簡単だったので、最小/最大を入力しました。

0
Migit

私は本当に遅いですが、ここに私の2セントがあります。

私は時間の値を5分間隔で切り上げおよび切り上げしたいと思っています。

10:31 --> 10:30
10:33 --> 10:35
10:36 --> 10:35

これは、TimeSpan.Tickに変換し、DateTimeに変換し直して、Math.Round()を使用することで実現できます。

public DateTime GetShiftedTimeStamp(DateTime timeStamp, int minutes)
{
    return
        new DateTime(
            Convert.ToInt64(
                Math.Round(timeStamp.Ticks / (decimal)TimeSpan.FromMinutes(minutes).Ticks, 0, MidpointRounding.AwayFromZero)
                    * TimeSpan.FromMinutes(minutes).Ticks));
}

上記のように、shiftedTimeStampはlinqグループ化で使用できます。

0
Jan