2012-01-13 16 views
27

Estoy tratando de comprender cómo se puede usar LINQ para agrupar datos por intervalos de tiempo; y luego idealmente agregue cada grupo.Agregado de LINQ y grupo por períodos de tiempo

Encontrando numerosos ejemplos con rangos de fechas explícitos, intento agrupar por períodos como 5 minutos, 1 hora, 1 día.

Por ejemplo, tengo una clase que se ajusta un DateTime con un valor de:

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

Estas observaciones están contenidas como una serie de una colección de lista:

List<Sample> series; 

Así, para agrupar por cada hora y el valor agregado en promedio, estoy tratando de hacer algo como:

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 el suyo es fundamentalmente defectuoso, ya que agrupa al TimeSpan mismo. No puedo entender cómo usar TimeSpan (o cualquier tipo de datos que represente un intervalo) en la consulta.

+1

¿Describiría su pregunta con datos de muestra? – Lrrr

+2

@AliAmiri - Creo que es lo suficientemente claro. Los resultados de muestra pueden ayudar. –

+0

Fantástica pregunta. Estoy seguro de que muchas personas luchan con esta tarea exacta. Parece que los datos de series temporales tienen su conjunto de dificultades. – Zapnologica

Respuesta

33

Se puede redondear la marca de tiempo al siguiente límite (es decir, hasta el más cercano límite de 5 minutos en el pasado) y usar eso como su agrupación: logra

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(); 

Por encima de que mediante el uso de un sello de tiempo modificado en la agrupación, que establece los minutos en el límite anterior de 5 minutos y elimina los segundos y milisegundos. El mismo enfoque, por supuesto, se puede usar para otros períodos de tiempo, es decir, horas y días.

Editar:

Basado en esta compuesto por entrada de la muestra:

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 grupos fueron producidos para mí, uno con la agrupación de marca de tiempo 3:05, uno con 03:10 y una con 3:20 p.m. (sus resultados pueden variar en función de la hora actual).

+0

¿Cuál es la diferencia entre su nuevo período de tiempo y los plazos disponibles para los artículos? usted acaba de cambiar el sesgo. – Lrrr

+0

@AliAmiri: agrupa los elementos que caen en el mismo intervalo de 5 minutos en el mismo grupo devolviendo la misma marca de tiempo para todos esos elementos. ¿No era eso lo que OP pretendía? – BrokenGlass

+0

No lo creo. Usted simplemente los mueve a 5 minutos antes (también no sé OP quiere hacer lo que intentó mostrar o no). – Lrrr

2

para agrupar por hora que necesitan agrupar por la parte de hora de su marca de tiempo que podría hacerse de modo que:

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

Se necesita una función que redondea sus timestampes. Algo como:

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

Para contenedores por hora. Y tenga en cuenta que la marca de tiempo en el resultado ahora será un DateTime, no un TimeSpan.


Editar, durante 5 minutos contenedores

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

¡Fenomenal !! ¡Esto también es lo que estaba buscando! ¡Muchas gracias! Aunque esto es más elegante, creo que voy a aceptar la respuesta de BrokenGlass ya que me permite agrupar por periodos como de 5 minutos, que creo que son los tuyos por unidad de tiempo, como segundos o minutos u horas, etc. pero no intervalos de 5 minutos. –

4

Estoy muy tarde al juego en este caso, pero me encontré con esto mientras que la búsqueda de algo más, y pensé que tenía una mejor camino.

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(); 

Aquí es un ejemplo de programa LINQPad para que pueda validar y probar

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

me gustaría sugerir el uso de nueva DateTime() a evitar cualquierproblemas con sub milisegundos diferencias

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

Con

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

N.B. Estoy aquí agrupando por Author.ID así como el TimeStamp redondeado.

función de rodeo tomado de @dtb respuesta aquí https://stackoverflow.com/a/7029464/661584

Lea sobre cómo la igualdad hasta la milésima de segundo no significa siempre la igualdad aquí Why does this unit test fail when testing DateTime equality?

0

Aunque soy muy tarde, aquí están mis 2 centavos:

quería Ronda() los valores de tiempo arriba y abajo en intervalos de 5 minutos:

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

Esta se puede lograr mediante la conversión a TimeSpan.Tick y convertir de nuevo a DateTime y usando 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)); 
} 

El shiftedTimeStamp se puede utilizar en agrupación linq como se muestra arriba.

0

Mejoré la respuesta de BrokenGlass haciéndola más genérica y agregué salvaguardas. Con su respuesta actual, si elige un intervalo de 9, no hará lo que espera. Lo mismo ocurre con cualquier número 60 no es divisible por. Para este ejemplo, estoy usando 9 y comenzando a la medianoche (0:00).

  • Todo de 0:00 a 0: 08.999 se pondrá en un grupo de 0:00 como era de esperar. Seguirá haciendo esto hasta llegar a la agrupación que comienza a 0:54.
  • En 0:54, solo se agruparán las cosas de 0:54 a 0: 59.999 en lugar de subir a 01: 03.999.

Para mí, esto es un problema enorme.

No estoy seguro de cómo solucionarlo, pero puede agregar protecciones.
Cambios:

  1. cualquier momento donde el 60% [intervalo] es igual a 0 será un intervalo aceptable. Las declaraciones if a continuación protegen esto.
  2. Los intervalos de hora también funcionan.

     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(); 
    

poner lo que desea en la declaración de selección! Puse min/max porque era más fácil probarlo.

0

Sé que esto no responde directamente a la pregunta, pero estaba buscando en Google una solución muy similar para agregar datos de velas para stocks/criptomonedas de un período de minutos más pequeño a un período de minutos superior (5, 10, 15, 30). No puede simplemente volver atrás del minuto actual tomando X a la vez, ya que las marcas de tiempo para los períodos agregados no serán consistentes. También debe tener en cuenta que hay datos suficientes al principio y al final de la lista para completar una vela completa del período más extenso. Dado que, la solución que se me ocurrió fue la siguiente. (Se supone que las velas para el período más pequeño, según lo indicado por rawPeriod, se ordenan por Timestamp ascendente).

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