Files
2025-07-02 13:31:56 +02:00

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