¿Cómo calcular la media móvil sin mantener el recuento y el total de datos?


118

Estoy tratando de encontrar una manera de calcular un promedio acumulativo móvil sin almacenar el recuento y los datos totales que se reciben hasta ahora.

Se me ocurrieron dos algoritmos, pero ambos necesitan almacenar el recuento:

  • nuevo promedio = ((recuento antiguo * datos antiguos) + siguiente dato) / siguiente recuento
  • nuevo promedio = promedio anterior + (datos siguientes - promedio anterior) / siguiente recuento

El problema con estos métodos es que el recuento aumenta cada vez más, lo que resulta en una pérdida de precisión en el promedio resultante.

El primer método utiliza el recuento anterior y el siguiente, que obviamente están separados por 1. Esto me hizo pensar que tal vez haya una forma de eliminar el recuento, pero desafortunadamente aún no lo he encontrado. Sin embargo, me llevó un poco más lejos, lo que resultó en el segundo método, pero aún está presente el recuento.

¿Es posible o solo estoy buscando lo imposible?


1
NB que numéricamente, almacenar el total actual y el recuento actual es la forma más estable. De lo contrario, para recuentos más altos, el siguiente / (siguiente recuento) comenzará a desbordar. Entonces, si está realmente preocupado por perder precisión, ¡quédese con los totales!
AlexR

Respuestas:


91

Simplemente puede hacer:

double approxRollingAverage (double avg, double new_sample) {

    avg -= avg / N;
    avg += new_sample / N;

    return avg;
}

¿Dónde Nestá el número de muestras sobre las que desea promediar? Tenga en cuenta que esta aproximación es equivalente a una media móvil exponencial. Ver: Calcular promedio móvil / móvil en C ++


3
¿No tiene que agregar 1 a N en esto antes de esta línea? avg + = nueva_muestra / N;
Damian

20
Esto no es del todo correcto. Lo que describe @Muis es un promedio móvil ponderado exponencialmente, que a veces es apropiado pero no es precisamente lo que solicitó el OP. Como ejemplo, considere el comportamiento que espera cuando la mayoría de los puntos están en el rango de 2 a 4 pero un valor es más de un millón. Un EWMA (aquí) conservará los rastros de ese millón durante bastante tiempo. Una convolución finita, como indica OP, la perdería inmediatamente después de N pasos. Tiene la ventaja de un almacenamiento constante.
jma

9
Eso no es un promedio móvil. Lo que describe es un filtro de un polo que crea respuestas exponenciales a los saltos en la señal. Una media móvil crea una respuesta lineal con una longitud de N.
brauner ruhig

3
Tenga en cuenta que esto está bastante lejos de la definición común de promedio. Si configura N = 5 e ingresa 5 5muestras, el promedio será 0.67.
Dan Dascalescu

2
@DanDascalescu Si bien tiene razón en que en realidad no es un promedio móvil, su valor declarado está desviado en un orden de magnitud. Con avginicializado en 0, termina 3.36después de 5 5s, y 4.46después de 10: cpp.sh/2ryql Para promedios largos, esta es sin duda una aproximación útil.
cincodenada

80
New average = old average * (n-1)/n + new value /n

Esto es asumiendo que el recuento solo cambió en un valor. En caso de que sea cambiado por valores M, entonces:

new average = old average * (n-len(M))/n + (sum of values in M)/n).

Esta es la fórmula matemática (creo que la más eficiente), creo que pueden hacer más código por ustedes mismos


¿Qué es la suma del nuevo valor? ¿Es eso diferente de alguna manera del "nuevo valor" en su fórmula original?
Mikhail

@Mikhail en el segundo ejemplo, se incluyen mnuevos valores en el nuevo promedio. Creo que sum of new valueaquí se pretende que sea la suma de los mnuevos valores que se utilizan para calcular el nuevo promedio.
Patrick Goley

9
Ligeramente más eficiente para el primero: new_average = (old_average * (n-1) + new_value) / n- Elimina una de las divisiones.
Pixelstix

¿Qué tal un promedio de ejecución de 3 elementos con 6,0,0,9?
Roshan Mehta

1
Cuando implemento esta ecuación, el valor o promedio móvil siempre aumenta lentamente. Nunca baja, solo sube.
anon58192932

30

De un blog sobre la ejecución de cálculos de varianza de muestra, donde la media también se calcula utilizando el método de Welford :

ingrese la descripción de la imagen aquí

Lástima que no podamos subir imágenes SVG.


3
Esto es similar a lo que implementó Muis, excepto que la división se usa como factor común. Por lo tanto, solo una división.
Flip

En realidad, está más cerca de @ Abdullah-Al-Ageel (esencialmente matemáticas conmutativas) en el sentido de que Muis no tiene en cuenta el incremento de N; Referencia de fórmula de copiar y pegar: [Avg at n] = [Avg at n-1] + (x - [Avg at n-1]) / n
drzaus

2
@Flip & drwaus: ¿No son las soluciones de Muis y Abdullah Al-Ageel exactamente iguales? Es el mismo cálculo, solo que escrito de manera diferente. Para mí, esas 3 respuestas son idénticas, esta es más visual (lástima que no podamos usar MathJax en SO).
user276648

21

Aquí hay otra respuesta que ofrece un comentario sobre cómo las respuestas de Muis , Abdullah Al-Ageel y Flip son matemáticamente iguales, excepto que están escritas de manera diferente.

Claro, tenemos el análisis de José Manuel Ramos que explica cómo los errores de redondeo afectan a cada uno de manera ligeramente diferente, pero eso depende de la implementación y cambiaría según la forma en que cada respuesta se aplique al código.

Sin embargo, hay una diferencia bastante grande

Está en Muis 's N, flip ' s k, y Abdullah Al-Ageel 's n. Abdullah Al-Ageel no explica lo que ndebería ser, pero Ny kse diferencian en que Nes " el número de muestras en las que desee promedio a lo largo ", mientras que kes el recuento de los valores muestreados. (Aunque tengo dudas sobre si llamar N al número de muestras es exacto).

Y aquí llegamos a la respuesta a continuación. Es esencialmente el mismo promedio móvil ponderado exponencial de edad que los demás, así que si estaba buscando una alternativa, deténgase aquí.

Media móvil ponderada exponencial

Inicialmente:

average = 0
counter = 0

Por cada valor:

counter += 1
average = average + (value - average) / min(counter, FACTOR)

La diferencia es la min(counter, FACTOR)parte. Esto es lo mismo que decir min(Flip's k, Muis's N).

FACTORes una constante que afecta la rapidez con que el promedio "se pone al día" con la última tendencia. Cuanto menor sea el número, más rápido. ( 1Ya no es un promedio y simplemente se convierte en el último valor).

Esta respuesta requiere el contador corriente counter. Si es problemático, min(counter, FACTOR)se puede reemplazar con solo FACTOR, convirtiéndolo en la respuesta de Muis . El problema de hacer esto es que la media móvil se ve afectada por lo que averagese inicialice. Si se inicializó en 0, ese cero puede tardar mucho en salir del promedio.

Como termina luciendo

Media móvil exponencial


3
Bien explicado. Solo pierdo un promedio simple en su gráfico, porque eso es lo que OP ha pedido.
xmedeko

Tal vez me esté perdiendo algo, pero tú, por casualidad, lo hiciste mal max(counter, FACTOR). min(counter, FACTOR)siempre devolverá FACTOR, ¿verdad?
WebWanderer

1
Creo que el punto min(counter, FACTOR)es tener en cuenta el período de calentamiento. Sin él, si su FACTOR (o N, o el recuento de muestras deseado) es 1000, entonces necesitará al menos 1000 muestras antes de obtener un resultado preciso, ya que todas las actualizaciones anteriores supondrán que tiene 1000 muestras, cuando solo puede tienen 20.
rharter

Sería bueno dejar de contar después de alcanzar el factor, probablemente sería más rápido de esa manera.
inf3rno

8

La respuesta de Flip es computacionalmente más consistente que la de Muis.

Usando el formato de doble número, podría ver el problema de redondeo en el enfoque de Muis:

El enfoque de Muis

Cuando divide y resta, aparece un redondeo en el valor almacenado anterior y lo cambia.

Sin embargo, el enfoque Flip conserva el valor almacenado y reduce el número de divisiones, por lo tanto, reduce el redondeo y minimiza el error propagado al valor almacenado. Agregar solo traerá redondeos si hay algo que agregar (cuando N es grande, no hay nada que agregar)

El enfoque Flip

Esos cambios son notables cuando haces que una media de valores grandes tiende a cero.

Te muestro los resultados usando un programa de hoja de cálculo:

En primer lugar, los resultados obtenidos: Resultados

Las columnas A y B son los valores n y X_n, respectivamente.

La columna C es el enfoque Flip y la D es el enfoque Muis, el resultado almacenado en la media. La columna E corresponde con el valor medio utilizado en el cálculo.

Un gráfico que muestra la media de valores pares es el siguiente:

Grafico

Como puede ver, existen grandes diferencias entre ambos enfoques.


2
No es realmente una respuesta, pero sí información útil. Sería incluso mejor si agregara la tercera línea a su gráfico, para el promedio real sobre n valores pasados, para que podamos ver cuál de los dos enfoques se acerca más.
jpaugh

2
@jpaugh: La columna B alterna entre -1,00E + 15 y 1,00E + 15, por lo que cuando N es par, la media real debería ser 0. El título del gráfico es "Medias parciales parciales". Esto significa que la tercera línea por la que preguntas es simplemente f (x) = 0. El gráfico muestra que ambos enfoques introducen errores que continúan aumentando.
Desowin

Eso es correcto, el gráfico muestra exactamente el error propagado usando números grandes involucrados en los cálculos usando ambos enfoques.
José Manuel Ramos

La leyenda de su gráfico tiene colores incorrectos: Muis es naranja, Flip es azul.
xmedeko

6

Un ejemplo usando javascript, a modo de comparación:

https://jsfiddle.net/drzaus/Lxsa4rpz/

function calcNormalAvg(list) {
    // sum(list) / len(list)
    return list.reduce(function(a, b) { return a + b; }) / list.length;
}
function calcRunningAvg(previousAverage, currentNumber, index) {
    // [ avg' * (n-1) + x ] / n
    return ( previousAverage * (index - 1) + currentNumber ) / index;
}


1

En Java8:

LongSummaryStatistics movingAverage = new LongSummaryStatistics();
movingAverage.accept(new data);
...
average = movingAverage.getAverage();

también tienes IntSummaryStatistics, DoubleSummaryStatistics...


2
OP está pidiendo un algoritmo, no un indicador de cómo calcular esto en Java.
olq_plo

0

Una solución de Python ordenada basada en las respuestas anteriores:

class RunningAverage():
    def __init__(self):
        self.average = 0
        self.n = 0
        
    def __call__(self, new_value):
        self.n += 1
        self.average = (self.average * (self.n-1) + new_value) / self.n 
        
    def __float__(self):
        return self.average
    
    def __repr__(self):
        return "average: " + str(self.average)

uso:

x = RunningAverage()
x(0)
x(2)
x(4)
print(x)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.