LINQ agrégé et groupé par périodes de temps

j'essaie de comprendre comment LINQ peut être utilisé pour grouper les données par intervalles de temps; et puis idéalement agréger chaque groupe.

trouver de nombreux exemples avec des plages de dates explicites, j'essaie de grouper par périodes telles que 5 minutes, 1 heure, 1 jour.

par exemple, j'ai une classe qui enveloppe une DateTime avec une valeur:

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

ces observations sont contenues dans une série D'une collection de listes:

List<Sample> series;

donc, pour grouper par périodes horaires de temps et valeur agrégée par moyenne, j'essaie de faire quelque chose comme:

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

c'est fondamentalement erroné, car il regroupe la durée elle-même. Je ne peux pas comprendre comment utiliser L'Intervalle de temps (ou n'importe quel type de données représentant un intervalle) dans la requête.

28
demandé sur Jason Sturges 2012-01-13 23:40:37

8 réponses

vous pouvez arrondir l'horodatage à la limite suivante (c.-à-d. vers le bas à la limite la plus proche de 5 minutes dans le passé) et utiliser cela comme votre groupe:

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

ci-dessus atteint cela en utilisant un horodatage modifié dans le groupement, qui fixe les minutes à la limite précédente de 5 minutes et enlève les secondes et les millisecondes. La même approche peut évidemment être utilisée pour d'autres périodes, c'est-à-dire des heures et des jours.

Edit:

sur la base de cette entrée d'échantillon composée:

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 groupes ont été produits pour moi, un avec le regroupement horodatage 3:05, Un avec 3:10 et un avec 3:20 pm (vos résultats peuvent varier en fonction de l'heure actuelle).

36
répondu BrokenGlass 2012-01-13 20:08:13

vous avez besoin d'une fonction qui fait le tour de vos horodateurs . Quelque chose comme:

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

pour les boîtes horaires. Et notez que l'horodatage dans le résultat sera désormais une DateTime, pas une durée.

9
répondu Henk Holterman 2018-09-16 08:49:34

je suis très en retard pour le jeu sur ce point, mais j'ai trouvé ceci en cherchant autre chose, et je pensais que j'avais une meilleure façon.

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

voici un exemple de programme linqpad pour que vous puissiez valider et tester ""

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;
}
4
répondu Duane McKinney 2014-01-24 22:41:01

pour grouper par heure vous avez besoin de grouper par heure partie de votre timestamp qui pourrait être fait ainsi:

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
répondu Michael 2012-01-13 20:15:22

je suggère à l'aide de new DateTime() à éviter tout problèmes avec les sous de la milliseconde différences

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

avec

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

N.B. je suis ici groupant par auteur.ID ainsi que l'horodatage arrondi.

fonction RoundUp tirée de @dtb réponse ici https://stackoverflow.com/a/7029464/661584

lire à propos de l'égalité jusqu'à la milliseconde ne signifie pas toujours égalité ici pourquoi ce test de l'unité échoue-t-il lors des tests d'égalité DateTime?

0
répondu MemeDeveloper 2017-05-23 11:47:13

même si je suis vraiment en retard, voici mes 2 cents:

je voulais Round() les valeurs de temps vers le bas ET jusqu'à 5 minutes d'intervalle:

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

cela peut être réalisé en convertissant en Durée.Cochez et convertissez de nouveau à DateTime et en utilisant les mathématiques.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));
}

le shiftedTimeStamp peut être utilisé dans le groupe linq comme indiqué ci-dessus.

0
répondu Jan 2014-12-16 21:35:40

j'ai amélioré la réponse de BrokenGlass en la rendant plus générique et en y ajoutant des garanties. Avec sa réponse, si vous avez choisi un intervalle de 9, il ne fera pas ce que vous attendez. Il en va de même pour tout nombre 60 n'est pas divisible par. Pour cet exemple, j'utilise 9 et je commence à minuit (0:00).

  • tout de 0: 00 à 0: 08.999 sera mis dans un groupe de 0:00 comme prévu. Il va continuer à faire cela jusqu'à ce que vous arrivez au groupement qui commence à 0 h 54.
  • à 0: 54, Il ne groupera que les choses de 0:54 à 0:59.999 au lieu de monter à 01:03.999.

Pour moi, c'est un énorme problème.

Je ne sais pas comment réparer ça, mais vous pouvez ajouter des sauvegardes.

Modifications:

  1. toute minute où 60 % [intervalle] est égal à 0 est un intervalle acceptable. Les énoncés ci-dessous relatifs à la fi le confirment.
  2. les Heures de travail.

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

mettez tout ce que vous voulez dans la déclaration select! J'ai mis min/max parce que c'était plus facile de le tester.

0
répondu Migit 2015-12-01 14:15:04

je sais que cela ne répond pas directement à la question, mais je googlais autour de la recherche d'une solution très similaire aux données agrégées de bougie pour les actions / monnaies de crypto d'une période de minute plus petite à une période de minute plus élevée(5, 10, 15, 30). Vous ne pouvez pas simplement revenir de la minute actuelle en prenant X à la fois, car les horodateurs pour les périodes agrégées ne seront pas cohérents. Vous devez également veiller à ce qu'il y ait suffisamment de données au début et à la fin de la liste pour remplir une chandelier de la période plus grande. Cela étant, la solution que j'ai trouvée était la suivante. (Il suppose que les bougies pour la période plus courte, comme indiqué par rawPeriod, sont triées par estampille temporelle ascendante.)

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
répondu vipes 2018-02-18 13:17:17