130 lines
6.3 KiB
C#
130 lines
6.3 KiB
C#
using MP.Data.DbModels;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace MP.Data.Services
|
|
{
|
|
public class TimeSeriesUtils
|
|
{
|
|
|
|
/// <summary>
|
|
/// Riduce una lista di oggetti FLModel mantenendo una spaziatura temporale uniforme.
|
|
/// Gli oggetti vengono raggruppati in intervalli temporali e viene selezionato un rappresentante per intervallo.
|
|
/// </summary>
|
|
/// <param name="data">La lista originale di oggetti FLModel.</param>
|
|
/// <param name="maxNum">Il numero massimo di oggetti desiderato nella lista risultante.</param>
|
|
/// <returns>Una nuova lista di FLModel con una distribuzione temporale più uniforme,
|
|
/// o la lista originale se già minore o uguale a maxNum.</returns>
|
|
public static List<FLModel> DownsampleFluxModels(List<FLModel> data, int maxNum)
|
|
{
|
|
// Caso base: se la lista è vuota o già abbastanza piccola, la restituiamo così com'è.
|
|
if (data == null || data.Count <= maxNum)
|
|
{
|
|
return data;
|
|
}
|
|
|
|
// 1. Ordina la lista per dtEvento. Questo è FONDAMENTALE per il campionamento temporale.
|
|
// Se la lista non è ordinata, il downsampling non funzionerà correttamente.
|
|
var sortedData = data.OrderBy(m => m.dtEvento).ToList();
|
|
|
|
// Determina l'intervallo temporale totale dei dati.
|
|
DateTime startTime = sortedData.First().dtEvento;
|
|
DateTime endTime = sortedData.Last().dtEvento;
|
|
|
|
// Calcola la durata totale in millisecondi (o tick per maggiore precisione).
|
|
long totalDurationTicks = endTime.Ticks - startTime.Ticks;
|
|
|
|
// Se tutti gli eventi avvengono nello stesso istante, non possiamo distribuirli.
|
|
if (totalDurationTicks == 0)
|
|
{
|
|
return sortedData.Take(maxNum).ToList();
|
|
}
|
|
|
|
// Inizializza la lista per i risultati campionati.
|
|
var sampledList = new List<FLModel>();
|
|
|
|
// 2. Calcola la dimensione di ciascun bucket temporale.
|
|
// Dividiamo la durata totale per il numero massimo desiderato di elementi.
|
|
// Questo ci dà la durata approssimativa di ciascun "bucket" temporale.
|
|
long ticksPerBucket = totalDurationTicks / maxNum;
|
|
|
|
// Garantisce che ticksPerBucket sia almeno 1 per evitare divisioni per zero o bucket infiniti.
|
|
if (ticksPerBucket == 0)
|
|
{
|
|
ticksPerBucket = 1;
|
|
}
|
|
|
|
// 3. Itera attraverso i dati e seleziona un rappresentante per ogni bucket.
|
|
// Useremo il primo elemento trovato in un nuovo bucket come rappresentante.
|
|
// Un'alternativa potrebbe essere prendere l'elemento più vicino al centro del bucket,
|
|
// o l'ultimo elemento del bucket. Per semplicità, scegliamo il primo.
|
|
|
|
// Inizializza il "target time" per il primo bucket.
|
|
DateTime currentBucketStartTime = startTime;
|
|
FLModel currentBucketRepresentative = null;
|
|
|
|
foreach (var item in sortedData)
|
|
{
|
|
// Se l'elemento corrente cade nel bucket attuale o in uno precedente,
|
|
// lo consideriamo per il bucket corrente.
|
|
// Aggiorniamo il rappresentante solo se è il primo trovato o se si vuole una logica diversa (es. ultimo elemento nel bucket).
|
|
if (item.dtEvento.Ticks >= currentBucketStartTime.Ticks && item.dtEvento.Ticks < currentBucketStartTime.Ticks + ticksPerBucket)
|
|
{
|
|
if (currentBucketRepresentative == null)
|
|
{
|
|
currentBucketRepresentative = item;
|
|
}
|
|
// Altre logiche per scegliere il rappresentante del bucket (es. ultimo, medio, ecc.)
|
|
// currentBucketRepresentative = item; // Se vuoi l'ultimo elemento del bucket
|
|
}
|
|
else
|
|
{
|
|
// Abbiamo superato il bucket corrente.
|
|
// Aggiungiamo il rappresentante del bucket precedente, se ne abbiamo trovato uno.
|
|
if (currentBucketRepresentative != null)
|
|
{
|
|
sampledList.Add(currentBucketRepresentative);
|
|
}
|
|
|
|
// Sposta al prossimo bucket. Dobbiamo farlo in un loop per coprire eventuali "salti"
|
|
// nel caso in cui non ci siano dati per diversi bucket consecutivi.
|
|
while (item.dtEvento.Ticks >= currentBucketStartTime.Ticks + ticksPerBucket)
|
|
{
|
|
currentBucketStartTime = new DateTime(currentBucketStartTime.Ticks + ticksPerBucket);
|
|
// Se non ci sono dati per un bucket, potremmo voler inserire un placeholder
|
|
// o semplicemente saltarlo. Qui lo saltiamo implicitamente.
|
|
}
|
|
|
|
// Inizia il nuovo bucket con l'elemento corrente come primo rappresentante.
|
|
currentBucketRepresentative = item;
|
|
}
|
|
|
|
// Se abbiamo già raggiunto il numero massimo desiderato, possiamo fermarci.
|
|
if (sampledList.Count >= maxNum)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Aggiungi l'ultimo rappresentante del bucket, se presente e se non abbiamo ancora raggiunto maxNum.
|
|
if (currentBucketRepresentative != null && sampledList.Count < maxNum)
|
|
{
|
|
sampledList.Add(currentBucketRepresentative);
|
|
}
|
|
|
|
// In alcuni rari casi (es. dati molto sparsi all'inizio o alla fine, o ticksPerBucket molto grandi)
|
|
// potremmo non raggiungere maxNum elementi ma comunque voler restituire i dati campionati.
|
|
// Se la lista campionata è ancora troppo piccola (meno di maxNum),
|
|
// potremmo voler applicare un'ulteriore logica di selezione o padding.
|
|
// Per questo problema specifico, l'algoritmo tende a dare al massimo `maxNum` elementi.
|
|
// Se il numero di bucket generati è maggiore di maxNum (potrebbe succedere con ticksPerBucket=1 e molti dati)
|
|
// la condizione `if (sampledList.Count >= maxNum)` sopra limiterà l'aggiunta.
|
|
|
|
return sampledList;
|
|
}
|
|
}
|
|
}
|