2012-01-13 5 views
27

Я пытаюсь понять, как LINQ можно использовать для группировки данных с интервалами времени; а затем идеально объединяют каждую группу.Объединение и группа LINQ по периодам времени

Поиск многочисленных примеров с явными диапазонами дат, я пытаюсь группировать по периодам, таким как 5 минут, 1 час, 1 день.

К примеру, у меня есть класс, который оборачивает в DateTime со значением:

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

Эти наблюдения содержатся в виде серии в коллекции List:

List<Sample> series; 

Так, в группе почасовые периоды времени и совокупное значение в среднем, я пытаюсь сделать что-то вроде:

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 }; 

T его принципиально недостаток, поскольку он группирует сам TimeSpan. Я не могу понять, как использовать TimeSpan (или любой тип данных, представляющий интервал) в запросе.

+1

Опишите свой вопрос с данными примера? – Lrrr

+2

@AliAmiri - Я думаю, что это достаточно ясно. Примеры результатов могут помочь. –

+0

Фантастический вопрос. Я уверен, что многие люди борются с этой точной задачей. Кажется, что данные временных рядов имеют множество трудностей. – Zapnologica

ответ

33

Вы можете округлить штамп времени до следующей границы (т.е. вниз до ближайшей 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 минут и удаляет секунды и миллисекунды. Тот же подход, конечно, может использоваться для других периодов времени, то есть часов и дней.

Edit:

На основании этого составлен ввод образца:

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 группа была подготовлена ​​для меня, один с группировкой метки времени 3:05, один с 3:10 и один с 15:20 (ваши результаты могут варьироваться в зависимости от текущего времени).

+0

В чем разница между вашим новым временем и доступными временными рамками для предметов? вы просто изменили предвзятость. – Lrrr

+0

@AliAmiri: он группирует элементы, которые попадают в один и тот же 5-минутный интервал в одну группу, возвращая одну и ту же метку времени для всех этих предметов - разве это не то, что предназначалось OP? – BrokenGlass

+0

Я так не думаю. Вы просто переместите их на 5 минут раньше (также я не знаю, что OP хочет делать то, что вы пытались показать или нет). – Lrrr

2

Для группировки по часам вам нужно сгруппировать по часовой части вашей временной метки, которые можно было бы сделать, как так:

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) 
             }; 
8

Вам нужна функция, которая округляет ваши timestampes. Что-то вроде:

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 }; 

Для почасовых бункеров. И обратите внимание, что timestamp в результате теперь будет DateTime, а не TimeSpan.


Edit, в течение 5 минут бункеров

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

Феноменальный !! Это то, что я искал! Спасибо! Хотя это более элегантно, я думаю, что я соглашусь ответить на вызов BrokenGlass, поскольку он позволяет мне группироваться по периодам, таким как 5 минут, которые, как я думаю, ваши группы составляют единицу времени, например, секунды или минуты или часы и т. Д. но не 5-минутные интервалы. –

4

Я очень поздно к игре на этом, но я наткнулся на это в то время искал что-то другое, и я думал, что я был лучше путь.

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; 
} 
0

я предлагаю использовать новый DateTime() для избежать каких-либо проблем с к югу миллисекунды различия

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 ответа здесь https://stackoverflow.com/a/7029464/661584

Узнайте о том, как равенство с точностью до миллисекунды, не всегда означает равенство здесь Why does this unit test fail when testing DateTime equality?

0

Даже если я очень поздно, вот мои 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

Я улучшил ответ от BrokenGlass, сделав его более универсальным и дополнительным защитным средством. С его текущим ответом, если вы выбрали интервал в 9, он не будет делать то, что вы ожидаете. То же самое и для любого числа 60 не делится на. В этом примере я использую 9 и начинаю в полночь (0:00).

  • Все с 0:00 до 0: 08.999 будет помещено в группу 0:00, как вы ожидали. Он будет продолжать делать это, пока не дойдете до группировки, которая начинается с 0:54.
  • В 0:54 он будет группировать вещи только с 0:54 до 0: 59.999 вместо перехода на 01: 03.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(); 
    

положить все, что вы хотите в отборном заявлении! Я положил min/max, потому что было легче протестировать его.

0

Я знаю, что это напрямую не отвечает на вопрос, но я искал очень похожее решение для сбора данных свечей для запасов/криптовалют с меньшего минутного периода до более высокой минуты (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; 
    }