Déterminer la moyenne et l'écart type en temps réel


31

Quelle serait la manière idéale de trouver la moyenne et l'écart type d'un signal pour une application en temps réel. Je voudrais pouvoir déclencher un contrôleur lorsqu'un signal était à plus de 3 écart-type de la moyenne pendant un certain temps.

Je suppose qu'un DSP dédié ferait cela assez facilement, mais y a-t-il un "raccourci" qui pourrait ne pas nécessiter quelque chose de si compliqué?


Savez-vous quelque chose sur le signal? Est-ce stationnaire?

@Tim Disons que c'est stationnaire. Pour ma propre curiosité, quelles seraient les ramifications d'un signal non stationnaire?
jonsca

3
S'il est stationnaire, vous pouvez simplement calculer une moyenne courante et un écart-type. Les choses seraient plus compliquées si la moyenne et l'écart-type variaient avec le temps.

Réponses:


36

Il y a une faille dans la réponse de Jason R, qui est discutée dans "Art of Computer Programming" vol. 2. Le problème survient si vous avez un écart-type qui est une petite fraction de la moyenne: le calcul de E (x ^ 2) - (E (x) ^ 2) souffre d'une sensibilité sévère aux erreurs d'arrondi à virgule flottante.

Vous pouvez même essayer cela vous-même dans un script Python:

ofs = 1e9
A = [ofs+x for x in [1,-1,2,3,0,4.02,5]] 
A2 = [x*x for x in A]
(sum(A2)/len(A))-(sum(A)/len(A))**2

J'obtiens -128.0 comme réponse, ce qui n'est clairement pas valable pour le calcul, car les mathématiques prédisent que le résultat devrait être non négatif.

Knuth cite une approche (je ne me souviens pas du nom de l'inventeur) pour calculer la moyenne courante et l'écart type qui ressemble à ceci:

 initialize:
    m = 0;
    S = 0;
    n = 0;

 for each incoming sample x:
    prev_mean = m;
    n = n + 1;
    m = m + (x-m)/n;
    S = S + (x-m)*(x-prev_mean);

puis après chaque étape, la valeur de mest la moyenne, et l'écart-type peut être calculé comme sqrt(S/n)ou sqrt(S/n-1)selon votre définition préférée de l'écart-type.

L'équation que j'écris ci-dessus est légèrement différente de celle de Knuth, mais elle est équivalente sur le plan des calculs.

Lorsque j'ai encore quelques minutes, je coderai la formule ci-dessus en Python et montrerai que vous obtiendrez une réponse non négative (qui, espérons-le, est proche de la valeur correcte).


mise à jour: le voici.

test1.py:

import math

def stats(x):
  n = 0
  S = 0.0
  m = 0.0
  for x_i in x:
    n = n + 1
    m_prev = m
    m = m + (x_i - m) / n
    S = S + (x_i - m) * (x_i - m_prev)
  return {'mean': m, 'variance': S/n}

def naive_stats(x):
  S1 = sum(x)
  n = len(x)
  S2 = sum([x_i**2 for x_i in x])
  return {'mean': S1/n, 'variance': (S2/n - (S1/n)**2) }

x1 = [1,-1,2,3,0,4.02,5] 
x2 = [x+1e9 for x in x1]

print "naive_stats:"
print naive_stats(x1)
print naive_stats(x2)

print "stats:"
print stats(x1)
print stats(x2)

résultat:

naive_stats:
{'variance': 4.0114775510204073, 'mean': 2.0028571428571427}
{'variance': -128.0, 'mean': 1000000002.0028572}
stats:
{'variance': 4.0114775510204073, 'mean': 2.0028571428571431}
{'variance': 4.0114775868357446, 'mean': 1000000002.0028571}

Vous remarquerez qu'il y a encore une erreur d'arrondi, mais ce n'est pas mauvais, alors naive_statsque ça vomit.


edit: Je viens de remarquer le commentaire de Belisarius citant Wikipedia qui mentionne l'algorithme Knuth.


1
+1 pour la réponse détaillée avec un exemple de code. Cette approche est supérieure à celle indiquée dans ma réponse lorsqu'une implémentation en virgule flottante est nécessaire.
Jason R

1
On pourrait également vérifier cela pour une implémentation C ++: johndcook.com/standard_deviation.html
Rui Marques

1
oui, c'est tout. Il utilise les équations exactes utilisées par Knuth. Vous pouvez optimiser quelque peu et éviter d'avoir à vérifier l'itération initiale par rapport aux itérations suivantes si vous utilisez ma méthode.
Jason S

"Knuth cite une approche (je ne me souviens pas du nom de l'inventeur) pour calculer la moyenne courante" - c'est d'ailleurs la méthode de Welford .
Jason S

J'ai posé une question à ce sujet si quelqu'un est en mesure d'aider: dsp.stackexchange.com/questions/31812/…
Jonathan

13

Quelle serait la manière idéale de trouver la moyenne et l'écart type d'un signal pour une application en temps réel. Je voudrais pouvoir déclencher un contrôleur lorsqu'un signal était à plus de 3 écart-type hors de la moyenne pendant un certain temps.

τ

Dans le domaine des fréquences, une "moyenne mobile exponentiellement pondérée" n'est qu'un pôle réel. Il est simple à mettre en œuvre dans le domaine temporel.

Implémentation du domaine temporel

Soit meanet meansqsoit les estimations actuelles de la moyenne et de la moyenne du carré du signal. À chaque cycle, mettez à jour ces estimations avec le nouvel échantillon x:

% update the estimate of the mean and the mean square:
mean = (1-a)*mean + a*x
meansq = (1-a)*meansq + a*(x^2)

% calculate the estimate of the variance:
var = meansq - mean^2;

% and, if you want standard deviation:
std = sqrt(var);

0<a<1a

Ce qui est exprimé ci-dessus comme un programme impératif peut également être représenté sous forme de diagramme de flux de signal:

entrez la description de l'image ici

Une analyse

yi=axi+(1a)yi1xiiyiz

H(z)=a1(1a)z1

En condensant les filtres IIR dans leurs propres blocs, le diagramme ressemble maintenant à ceci:

entrez la description de l'image ici

z=esTTfs=1/T1(1a)esT=0s=1Tlog(1a)

a

a=1exp{2πTτ}

Les références


1
aa0 > a > 1

Ceci est similaire à l'approche de Jason R. Cette méthode sera moins précise mais un peu plus rapide et plus faible en mémoire. Cette approche finit par utiliser une fenêtre exponentielle.
schnarf

Woops! Bien sûr, je voulais dire 0 < a < 1. Si votre système a un échantillonnage tmie Tet que vous souhaitez une constante de temps moyenne tau, choisissez a = 1 - exp (2*pi*T/tau).
nibot

Je pense qu'il peut y avoir une erreur ici. Les filtres unipolaires n'ont pas de gain de 0 dB à DC et puisque vous appliquez un filtre dans le domaine linéaire et un dans le domaine carré, l'erreur de gain est différente pour E <x> et E <x ^ 2>. Je développerai plus dans ma réponse
Hilmar

Il a un gain de 0 dB à DC. Remplacez z=1(DC) en H(z) = a/(1-(1-a)/z)et vous obtenez 1.
nibot

5

Une méthode que j'ai utilisée auparavant dans une application de traitement intégrée consiste à conserver les accumulateurs de la somme et de la somme des carrés du signal d'intérêt:

Ax,i=k=0ix[k]=Ax,i1+x[i],Ax,1=0

Ax2,i=k=0ix2[k]=Ax2,i1+x2[i],Ax2,1=0

ii

μ~=Axii+1

σ~=Axi2i+1μ~2

ou vous pouvez utiliser:

σ~=Axi2iμ~2

selon la méthode d'estimation de l'écart type que vous préférez . Ces équations sont basées sur la définition de la variance :

σ2=E(X2)(E(X))2

Je les ai utilisés avec succès dans le passé (bien que je ne me préoccupe que de l'estimation de la variance, pas de l'écart-type), bien que vous deviez faire attention aux types numériques que vous utilisez pour contenir les accumulateurs si vous voulez résumer une longue période de temps; vous ne voulez pas de débordement.

Edit: En plus du commentaire ci-dessus sur le débordement, il convient de noter que ce n'est pas un algorithme numériquement robuste lorsqu'il est implémenté en arithmétique à virgule flottante, ce qui peut potentiellement provoquer de grandes erreurs dans les statistiques estimées. Regardez la réponse de Jason S pour une meilleure approche dans ce cas.


1
Ax,i=x[i]+Ax,i1, Ax,0=x[0]ix

Oui, c'est mieux. J'ai essayé de réécrire pour rendre l'implémentation récursive plus claire.
Jason R

2
-1 quand j'ai assez de représentants pour le faire: cela a des problèmes numériques. Voir Knuth vol. 2
Jason S

σμ2σ2=E(X2)(E(X))2

2
@JasonS: Je ne serais pas d'accord pour dire que la technique est intrinsèquement défectueuse, bien que je convienne avec votre point que ce n'est pas une méthode numériquement robuste lorsqu'elle est mise en œuvre en virgule flottante. J'aurais dû être plus clair que j'ai utilisé cela avec succès dans une application qui utilisait l' arithmétique entière . L'arithmétique entière (ou implémentations à virgule fixe des nombres fractionnaires) ne souffre pas du problème que vous avez signalé et qui entraîne une perte de précision. Dans ce contexte, il s'agit d'une méthode appropriée qui nécessite moins d'opérations par échantillon.
Jason R

3

Semblable à la réponse préférée ci-dessus (Jason S.), et dérivée également de la formule tirée de Knut (Vol.2, p 232), on peut également dériver une formule pour remplacer une valeur, c'est-à-dire supprimer et ajouter une valeur en une seule étape . Selon mes tests, replace offre une meilleure précision que la version supprimer / ajouter en deux étapes.

Le code ci-dessous est en Java, meanet sest mis à jour (variables membres "globales"), comme ci m- sdessus et dans la publication de Jason. La valeur countfait référence à la taille de la fenêtre n.

/**
 * Replaces the value {@code x} currently present in this sample with the
 * new value {@code y}. In a sliding window, {@code x} is the value that
 * drops out and {@code y} is the new value entering the window. The sample
 * count remains constant with this operation.
 * 
 * @param x
 *            the value to remove
 * @param y
 *            the value to add
 */
public void replace(double x, double y) {
    final double deltaYX = y - x;
    final double deltaX = x - mean;
    final double deltaY = y - mean;
    mean = mean + deltaYX / count;
    final double deltaYp = y - mean;
    final double countMinus1 = count - 1;
    s = s - count / countMinus1 * (deltaX * deltaX - deltaY * deltaYp) - deltaYX * deltaYp / countMinus1;
}

3

La réponse de Jason et Nibot diffère sur un aspect important: la méthode de Jason calcule le dev std et la moyenne pour le signal entier (puisque y = 0), tandis que Nibot est un calcul "courant", c'est-à-dire qu'il pèse des échantillons plus récents plus forts que les échantillons du passé lointain.

Étant donné que l'application semble nécessiter un dev std et une moyenne en fonction du temps, la méthode de Nibot est probablement la plus appropriée (pour cette application spécifique). Cependant, la vraie partie délicate sera d'obtenir la bonne pondération temporelle. L'exemple de Nibot utilise un simple filtre unipolaire.

E[x]x[n]E[x2]x[n]2

Le choix du filtre passe-bas peut être guidé par ce que vous savez sur votre signal et la résolution temporelle dont vous avez besoin pour votre estimation. Des fréquences de coupure plus basses et un ordre plus élevé se traduiront par une meilleure précision mais un temps de réponse plus lent.

Pour compliquer encore les choses, un filtre est appliqué dans le domaine linéaire et un autre dans le domaine carré. La quadrature modifie considérablement le contenu spectral du signal, vous pouvez donc utiliser un filtre différent dans le domaine carré.

Voici un exemple sur la façon d'estimer la moyenne, la valeur efficace et la déviation standard en fonction du temps.

%% example
fs = 44100; n = fs; % 44.1 kHz sample rate, 1 second
% signal: white noise plus a low frequency drift at 5 Hz)
x = randn(n,1) + sin(2*pi*(0:n-1)'*5/fs);
% mean estimation filter: since we are looking for effects in the 5 Hz range we use maybe a
% 25 Hz filter, 2nd order so it's not too sluggish
[b,a] = butter(2,25*2/fs);
xmeanEst = filter(b,a,x);
% now we estimate x^2, since most frequency double we use twice the bandwidth
[b,a] = butter(2,50*2/fs);
x2Est = filter(b,a,x.^2);
% std deviation estimate
xstd = sqrt(x2Est)-xmeanEst;
% and plot it
h = plot([x, xmeanEst sqrt(x2Est) xstd]);
grid on;
legend('x','E<x>','sqrt(E<x^2>)','Std dev');
set(h(2:4),'Linewidth',2);

1
Le filtre dans ma réponse correspond à y1 = filter(a,[1 (1-a)],x);.
nibot

1
Bon point sur la distinction entre les statistiques de fonctionnement et les statistiques de l'échantillon global. Mon implémentation pourrait être modifiée pour calculer les statistiques de fonctionnement en s'accumulant sur une fenêtre mobile, ce qui peut également être fait efficacement (à chaque pas de temps, soustrayez l'échantillon de temps qui vient de sortir de la fenêtre de chaque accumulateur).
Jason R

nibot, désolé tu as raison et j'avais tort. Je vais corriger ça tout de suite
Hilmar

1
+1 pour avoir suggéré un filtrage différent pour x et x ^ 2
nibot
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.