c# linq aggregate-functions

c# - LINQ agregado y grupo por períodos de tiempo



aggregate-functions (8)

Intento entender cómo se puede usar LINQ para agrupar datos por intervalos de tiempo; y luego idealmente agregue cada grupo.

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

Por ejemplo, tengo una clase que envuelve un DateTime con un valor:

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

Estas observaciones se incluyen como una serie en una colección de Lista:

List<Sample> series;

Entonces, para agrupar por períodos de tiempo por hora y valor agregado por promedio, intento 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 };

Esto es fundamentalmente defectuoso, ya que agrupa el TimeSpan mismo. No puedo entender cómo usar TimeSpan (o cualquier tipo de datos que represente un intervalo) en la consulta.


Aunque estoy muy retrasado, aquí están mis 2 centavos:

Quería redondear () los valores de tiempo hacia abajo Y hacia arriba en intervalos de 5 minutos:

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

Esto se puede lograr convirtiendo a TimeSpan.Tick y convirtiendo 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 usar en la agrupación linq como se muestra arriba.


Llegué muy tarde al juego en este caso, pero me encontré con esto mientras buscaba otra cosa, y pensé que tenía una mejor manera.

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í hay un programa de ejemplo de 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; }


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 lo que se espere de 0:00 a 0: 08.999 se incluirá en un grupo de 0:00. 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í, este es un problema masivo.

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

  1. Cualquier minuto donde 60% [intervalo] sea 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();

¡Pon lo que quieras en la declaración seleccionada! Puse min / max porque era más fácil probarlo.


Necesita una función que redondee su tiempo de ejecución. 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 de hora. Y tenga en cuenta que la marca de tiempo en el resultado ahora será un DateTime, no un TimeSpan.

Editar, por intervalos de 5 minutos

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


Para agrupar por hora, debe agrupar por hora la parte de su marca de tiempo, que podría hacerse de la siguiente manera:

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


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

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

Arriba lo logra utilizando una marca de tiempo modificada 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 utilizar para otros períodos de tiempo, es decir, horas y días.

Editar:

Basado en esta entrada de muestra compuesta:

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

Se produjeron 3 grupos para mí, uno con la marca de tiempo de agrupamiento 3:05, uno con 3:10 y otro con 3:20 pm (los resultados pueden variar en función de la hora actual).


Sé que esto no responde directamente la pregunta, pero busqué en Google una solución muy similar para agregar datos de velas para acciones / 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. (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; }


Sugeriría usar el nuevo DateTime () para evitar cualquier problema con diferencias de menos de milisegundos

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

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

Función RoundUp tomada de @dtb answer here https://.com/a/7029464/661584

Lea acerca de cómo la igualdad hasta el milisegundo no siempre significa igualdad aquí ¿Por qué esta prueba de unidad falla al probar la igualdad de fecha y hora?