Подтвердить что ты не робот

Состав и группа 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 };

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

4b9b3361

Ответ 1

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

Ответ 2

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

 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.

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

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

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.

Функция RoundUp, взятая из @dtb, отвечает здесь fooobar.com/questions/52370/...

Прочитайте, как равенство до миллисекунды не всегда означает равенство здесь Почему этот unit test не работает при тестировании равенства DateTime?

Ответ 6

Хотя я действительно опаздываю, вот мои 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));
}

СдвинутыйTimeStamp может использоваться в группировке linq, как показано выше.

Ответ 7

Я улучшил ответ 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.

Для меня это серьезная проблема.

Я не уверен, как это исправить, но вы можете добавить меры предосторожности. Изменения:

  • Любая минута, где 60% [интервал] равен 0, будет приемлемым интервалом. Приведенные ниже операторы if гарантируют это.
  • Часовые интервалы также работают.

            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! Я положил min/max, потому что было легче протестировать его.