J'essaie de comprendre comment LINQ peut être utilisé pour regrouper des données par intervalles de temps; et ensuite, idéalement, agréger chaque groupe.
Trouvant de nombreux exemples avec des plages de dates explicites, j'essaie de regrouper des périodes telles que 5 minutes, 1 heure, 1 jour.
Par exemple, j'ai une classe qui enveloppe un DateTime avec une valeur:
public class Sample
{
public DateTime timestamp;
public double value;
}
Ces observations sont contenues sous forme de série dans une collection List:
List<Sample> series;
Donc, pour regrouper par périodes horaires et la valeur globale en 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 };
Ceci est fondamentalement défectueux, car il regroupe le TimeSpan lui-même. Je ne comprends pas comment utiliser TimeSpan (ou tout type de données représentant un intervalle) dans la requête.
Vous pouvez arrondir l’horodatage jusqu’à la limite suivante (c’est-à-dire jusqu’à la limite des 5 minutes la plus proche dans le passé) et l’utiliser comme 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 permet d'atteindre cet objectif en utilisant un horodatage modifié dans le groupe, ce qui permet de définir les minutes sur la limite des 5 minutes précédentes et de supprimer les secondes et les millisecondes. La même approche peut bien sûr être utilisée pour d’autres périodes, c’est-à-dire des heures et des jours.
Modifier:
Sur la base de cet échantillon composé:
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) });
Trois groupes ont été créés pour moi, l'un avec un horodatage de regroupement de 3h05, un avec 15h10 et un avec 15h20 (vos résultats peuvent varier en fonction de l'heure actuelle).
Vous avez besoin d'une fonction qui arrondit vos horodatages. 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 bacs horaires. Et notez que l'horodatage dans le résultat sera désormais un DateTime, pas un TimeSpan.
Je suis très tard pour le match sur ce coup-là, mais je l'ai compris en cherchant autre chose, et je pensais que j'avais un meilleur moyen.
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 afin 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;
}
Pour grouper par heure, vous devez regrouper par heure une partie de votre horodatage, ce qui pourrait se faire de la manière suivante:
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)
};
Je suggérerais d'utiliser new DateTime () pour éviter tout problème avec des différences inférieures à la milliseconde
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 regroupant par Author.ID ainsi que le TimeStamp arrondi.
Fonction RoundUp tirée de @dtb answer here https://stackoverflow.com/a/7029464/661584
Découvrez comment l'égalité en millisecondes ne signifie pas toujours l'égalité ici Pourquoi ce test unitaire échoue-t-il lors de la vérification de l'égalité DateTime?
Je sais que cela ne répond pas directement à la question, mais je cherchais une solution très similaire pour regrouper les données de bougies pour les actions/monnaies cryptographiques d'une minute à une minute . Vous ne pouvez pas simplement revenir en arrière à partir de la minute en cours en prenant X à la fois, car les horodatages des périodes agrégées ne seront pas cohérents. Vous devez également vous assurer qu'il y a suffisamment de données au début et à la fin de la liste pour renseigner un chandelier complet de la période la plus longue. Compte tenu de cela, la solution que j'ai trouvée était la suivante. (Cela suppose que les bougies pour la période la plus courte, comme indiqué par rawPeriod, sont triées par horodatage croissant.)
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;
}
J'ai amélioré la réponse de BrokenGlass en la rendant plus générique et en y ajoutant des garde-fous. Avec sa réponse actuelle, si vous choisissez un intervalle de 9, il ne fera pas ce que vous attendez. Il en va de même pour tout nombre 60 non divisible par. Pour cet exemple, j'utilise 9 et commence à minuit (0:00).
Pour moi, c'est un problème énorme.
Je ne sais pas comment résoudre ce problème, mais vous pouvez ajouter des garde-fous.
Changements:
Les intervalles horaires fonctionnent également.
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 ce que vous voulez dans la déclaration select! J'ai mis en min/max car il était plus facile de le tester.
Même si je suis vraiment en retard, voici mes 2 centimes:
Je voulais arrondir () les valeurs de temps vers le bas ET À intervalles de 5 minutes:
10:31 --> 10:30
10:33 --> 10:35
10:36 --> 10:35
Pour ce faire, convertissez TimeSpan.Tick, reconvertissez DateTime et utilisez 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));
}
ShiftedTimeStamp peut être utilisé dans le groupement linq comme indiqué ci-dessus.