Media a finestra mobile

Ciao a tutti.
Volevo proporre un metodo diverso per calcolare la media di una serie continua di valori, che io uso spesso e che spero faccia piacere anche a voi conoscere.
Nel forum qualcuno ne parla ma nessuno ne spiega il funzionamento e l'implementazione in Arduino.

Parto col dire che la media a finestra mobile viene utilizzata per calcolare la media di un sotto gruppo di una serie di valori ma, essendo questi valori molti di più, la media viene ricalcolata ogni volta togliendo dal sotto gruppo il valore acquisito più vecchio ed inserendo al suo posto un nuovo valore.
Ovviamente, per definizione di media, anche questa restituirà un valore solo.
Si definisce "finestra" il numero di valori di cui si vuole calcolare la media, e "passo" il numero di valori di cui spostiamo la finestra in avanti.
Il vantaggio è nel tempo utilizzato per l'acquisizione e il calcolo della media che è notevolmente più basso a quello impiegato per il calcolo della media col mettodo "classico".
Lo svantaggio, è l'utilizzo di un linguaggio più "complesso" per lo sketch.
Ma andiamo al lato pratico.
Quando vogliamo calcolare la media, ad esempio, delle temperature rilevate da un sensore, dobbiamo acquisire (sempre per esempio) 10 valori, sommarli tra loro e dividerli per 10.
Per la media a finestra mobile il discorso è più lungo: acquisiamo 10 valori (finestra) in un array (più facile da utilizzare piuttosto di avere 10 variabili diverse) e calcolare la media dei 10 valori, fare uno shift a destra o a sinistra di un numero tot di valori (passo della finestra), acquisire tanti valori quanto è il passo della finestra, ricalcolare la nuova media, e riprendere il ciclo dallo shift.
L'accortezza da prendere è quella che l'array in cui memorizziamo i valori letti, all'inizio è vuoto e va quindi popolato all'avvio del programma.

Per semplicità, utilizzo una finestra di 10 valori ed un passo di 1.

Ho calcolato il tempo impiegato da Arduino ethernet a fare le varie operazioni ed ho ottenuto questi valori:
-Tempo di acquisizione di 10 valori= 1220uS
-Tempo di acquisizione analogico di un valore (mediamente)=112uS
-Tempo di shift di 10 valori=12uS
-Tempo per sommare 10 valori (da un array)=12uS

La media "classica", impiega in tutto 1220uS su 10 valori, mentre la media a finestra mobile impiega la prima volta 1220uS (perchè l'array va popolato), poi solo 136uS!

Implementazione:

Acquisizione di 10 valori per popolare l'array (1220uS):

for(i=0; i<10; i++){
    temp_array[i]=analogRead(A0);
  }

Calcolo della media (12uS):

for(i=0; i<10; i++)
    {
      somma += temp_array[i];
    }
somma=somma/10;

Shift a destra (12uS):

for(i=0; i<9; i++) \\Lasciamo uno spazio vuoto per un nuovo valore
    {
      temp_array[i]=temp_array[i+1];
    }

Acquisizione di un nuovo valore (112uS):

temp_array[9]=analogRead(A0);

Poi il loop riprende dal calcolo della media.

Come esempio allego il grafico dello schema a blocchi e della temperatura dell'acqua della mia caldaia, rilevata da un sensore posto sul tubo che esce dalla caldaia stessa.
La serie in blu dà il valore così come il sensore l'ha rilevata, in viola applicando la media a finestra mobile con finestra di 10 e passo 1, in giallo la media con finestra 5 e passo 1.
ATTENZIONE: L'allineamento del grafico della media è a sinistra, nel senso che il valore della media è allineato al primo valore a sinistra del grafico non mediato!

EDIT:
Inserisco qui la versione con buffer circolare come suggerito da RobertoBochet e astrobeed.
Ho creato da zero la mia versione (molto più facile da capire) perchè quella suggerita da Roberto era troppo complicata per i miei gusti.
Il buffer, dopo averlo popolato all'inizio viene gestito dalla funzione buffer:

float buffer(int value){
  if(i>=10)i=0;
  somma=somma-array[i]+value;
  array[i]=value;
  i++;
  return somma/10.0;
}

Questa funzione riceve in ingresso il valore attuale (value), lo aggiunge alla somma e toglie il valore precedentemente memorizzato nel buffer in posizione i.
Poi memorizza al suo posto il valore attuale, calcola la media e la restituisce al chiamante tramite il return.
Il tempo impiegato per tutti questi passaggi è di soli 8uS.
Il nuovo schema a blocchi sarebbe composto dalla sola acquisizione di un valore e dalla funzione buffer.
In base ai tempi precedentemente descitti, il tutto si compie in 120uS!!
Esempio senza popolazione dell'array all'inizio del programma:

int array[10];
int i=0;
int somma=0;
float buffer(int value){
  if(i>=10)i=0;
  somma=somma-array[i]+value;
  array[i]=value;
  i++;
  return somma/10.0;
}
void setup() {
  Serial.begin(9600);
}
void loop(){
  float media;
  int a=analogRead(A0);
  media=buffer(a);
  Serial.print("Valore letto: ");
  Serial.println(a);
  Serial.print("Media: ");
  Serial.println(media);
  delay (1000);
}

Schema a blocchi.JPG

Bravo, mi permetto di sottolineare una piccola ottimizzazione che può rendere il tuo sistema molto più rapido. Lo shift in array è un sistema molto lento, soprattutto per i array molto grossi, eseguire uno shift a destra di 50 valori può risultare un compito molto pesante per una piccola MCU, per questo si impiegano forme di archiviazione leggermente più evolute come il buffer circolare. Questa tipologia di buffer non ha un capo e una coda fisse, ma questi girano sui valori ad ogni shift, evitando di dover spostare alcun valore.
Inoltre sommare ogni volta da 0 i valori aggiunge un ennesima ridondanza che rallenta il processo, puoi sommare tutti i valori e man mano che i valori escono dal buffer li sottrai alla somma.
Mi permetto inoltre di postarti un implementazione in C di questo metodo per media mobile semplice(SMA) e ponderata(WMA). link
Spero di esserti stato utile. Continua ad usare la SMA :wink:

1 Like

Concordo al 100%, usare il buffer circolare e sottrarre il valore più vecchio prima di sostituirlo con quello nuovo, si risparmia molto tempo cpu, sopratutto nel caso di medie mobili con grossi buffer.
Un ulteriore dettaglio, la differenza tra due successivi valori della media mobile fornisce il valore, abbastanza preciso, della derivata della grandezza fisica in esame, valore molto utile quando si implementano controlli.

L'uso del buffer circolare non lo avevo mai associato alla media a finestra mobile. Giusto per completezza del post, potreste riportare le righe di codice che lo implementano?
Altrimenti, non appena ho un po' di tempo lo faccio io.
Ottimo consiglio!

Ti ho postato nel post precedente un link ad un implementazione della SMA ottimizzata con buffer circolare e somma e sottrazione dei valori. Per di più commentata in Italiano, non hai scuse :stuck_out_tongue:

Grazie RobertoBochet..sei un vero amico! :smiley:
Modifico il post iniziale così da avere le due versioni a portata di mano.

La piu grande differenza che si puo notare è che il mio algoritmo tiene traccia del numero di elementi inseriti nel buffer, rendendo il tutto meno efficiente, è un complessità aggiuntiva che mi sono voluto permette per avere una finestra mobile piu grande del numero effettivo di campioni che voglio considerare, in poche parole posso variare il numero di elementi a runtime senza dover fare strani giri. Generalmente non ne vale la pena, ho cercato di rendere il tutto piu versatile, ma forse sarà meglio optare per una strategia piu simile alla tua.

Scrivere

sma->beginningArray + sma->elements - 1

o

&sma->beginnigArray[sma->elements]

in assembly sono equivalenti, è solo una preferenza personale usare i puntatori invece degli array.

Appena ho un attimo vedo se si puo ottimizzare un po'.

Io non uso i puntatori perchè sono un po' arrugginito sull'argomento e rischio di intripparmi e perchè il listato risulta molto criptico.
Nel mio codice ho usato variabili globali per gestire gli array così che non vengano cancellate da un richiamo alla funzione buffer all'altro, come l'indice i.

Qualitatore:
Io non uso i puntatori perchè sono un po' arrugginito sull'argomento e rischio di intripparmi e perchè il listato risulta molto criptico.

ehh ma sono da imparare bene i puntatori, ora che hai alle spalle il wiring ti salvi, se capiterà mai che dovrai scrivere un listato direttamente in C per un AVR(se ancora esisteranno :slightly_frowning_face: ) senza padroneggiare i puntatori non vai lontano.