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