Este problema requiere un puntaje z o puntaje estándar, que tendrá en cuenta el promedio histórico, como han mencionado otras personas, pero también la desviación estándar de estos datos históricos, lo que lo hace más robusto que el simple uso del promedio.
En su caso, la siguiente fórmula calcula un puntaje z, donde la tendencia sería una tasa tal como vistas / día.
z-score = ([current trend] - [average historic trends]) / [standard deviation of historic trends]
Cuando se utiliza un puntaje z, cuanto mayor o menor es el puntaje z, más anormal es la tendencia, por ejemplo, si el puntaje z es altamente positivo, entonces la tendencia aumenta de manera anormal, mientras que si es altamente negativo, disminuye anormalmente . Entonces, una vez que calcule el puntaje z para todas las tendencias candidatas, los 10 puntajes z más altos se relacionarán con los puntajes z más anormales.
Consulte Wikipedia para obtener más información sobre las puntuaciones z.
Código
from math import sqrt
def zscore(obs, pop):
# Size of population.
number = float(len(pop))
# Average population value.
avg = sum(pop) / number
# Standard deviation of population.
std = sqrt(sum(((c - avg) ** 2) for c in pop) / number)
# Zscore Calculation.
return (obs - avg) / std
Salida de muestra
>>> zscore(12, [2, 4, 4, 4, 5, 5, 7, 9])
3.5
>>> zscore(20, [21, 22, 19, 18, 17, 22, 20, 20])
0.0739221270955
>>> zscore(20, [21, 22, 19, 18, 17, 22, 20, 20, 1, 2, 3, 1, 2, 1, 0, 1])
1.00303599234
>>> zscore(2, [21, 22, 19, 18, 17, 22, 20, 20, 1, 2, 3, 1, 2, 1, 0, 1])
-0.922793112954
>>> zscore(9, [1, 2, 0, 3, 1, 3, 1, 2, 9, 8, 7, 10, 9, 5, 2, 4, 1, 1, 0])
1.65291949506
Notas
Puede usar este método con una ventana deslizante (es decir, los últimos 30 días) si no desea tener en cuenta demasiado historial, lo que hará que las tendencias a corto plazo sean más pronunciadas y reduzca el tiempo de procesamiento.
También puede usar una puntuación z para valores como el cambio en las vistas de un día al día siguiente para ubicar los valores anormales para aumentar / disminuir las vistas por día. Esto es como usar la pendiente o derivada del gráfico de vistas por día.
Si realiza un seguimiento del tamaño actual de la población, el total actual de la población y el total actual de x ^ 2 de la población, no necesita recalcular estos valores, solo actualícelos y, por lo tanto, solo necesita mantenga estos valores para el historial, no cada valor de datos. El siguiente código demuestra esto.
from math import sqrt
class zscore:
def __init__(self, pop = []):
self.number = float(len(pop))
self.total = sum(pop)
self.sqrTotal = sum(x ** 2 for x in pop)
def update(self, value):
self.number += 1.0
self.total += value
self.sqrTotal += value ** 2
def avg(self):
return self.total / self.number
def std(self):
return sqrt((self.sqrTotal / self.number) - self.avg() ** 2)
def score(self, obs):
return (obs - self.avg()) / self.std()
Con este método, su flujo de trabajo sería el siguiente. Para cada tema, etiqueta o página, cree un campo de punto flotante, para el número total de días, la suma de vistas y la suma de vistas al cuadrado en su base de datos. Si tiene datos históricos, inicialice estos campos utilizando esos datos, de lo contrario, inicialícelos a cero. Al final de cada día, calcule el puntaje z utilizando el número de vistas del día contra los datos históricos almacenados en los tres campos de la base de datos. Los temas, las etiquetas o las páginas con los puntajes z X más altos son sus X "tendencias más populares" del día. Finalmente actualice cada uno de los 3 campos con el valor del día y repita el proceso mañana.
Nueva adquisición
Los puntajes z normales como se discutió anteriormente no tienen en cuenta el orden de los datos y, por lo tanto, el puntaje z para una observación de '1' o '9' tendría la misma magnitud contra la secuencia [1, 1, 1, 1 , 9, 9, 9, 9]. Obviamente para la búsqueda de tendencias, los datos más actuales deberían tener más peso que los datos más antiguos y, por lo tanto, queremos que la observación '1' tenga una puntuación de magnitud mayor que la observación '9'. Para lograr esto, propongo una puntuación z promedio flotante. Debe quedar claro que NO se garantiza que este método sea estadísticamente sólido, pero debería ser útil para la búsqueda de tendencias o similar. La principal diferencia entre el puntaje z estándar y el puntaje z promedio flotante es el uso de un promedio flotante para calcular el valor promedio de la población y el valor promedio de la población al cuadrado. Ver código para más detalles:
Código
class fazscore:
def __init__(self, decay, pop = []):
self.sqrAvg = self.avg = 0
# The rate at which the historic data's effect will diminish.
self.decay = decay
for x in pop: self.update(x)
def update(self, value):
# Set initial averages to the first value in the sequence.
if self.avg == 0 and self.sqrAvg == 0:
self.avg = float(value)
self.sqrAvg = float((value ** 2))
# Calculate the average of the rest of the values using a
# floating average.
else:
self.avg = self.avg * self.decay + value * (1 - self.decay)
self.sqrAvg = self.sqrAvg * self.decay + (value ** 2) * (1 - self.decay)
return self
def std(self):
# Somewhat ad-hoc standard deviation calculation.
return sqrt(self.sqrAvg - self.avg ** 2)
def score(self, obs):
if self.std() == 0: return (obs - self.avg) * float("infinity")
else: return (obs - self.avg) / self.std()
Muestra IO
>>> fazscore(0.8, [1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9]).score(1)
-1.67770595327
>>> fazscore(0.8, [1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9]).score(9)
0.596052006642
>>> fazscore(0.9, [2, 4, 4, 4, 5, 5, 7, 9]).score(12)
3.46442230724
>>> fazscore(0.9, [2, 4, 4, 4, 5, 5, 7, 9]).score(22)
7.7773245459
>>> fazscore(0.9, [21, 22, 19, 18, 17, 22, 20, 20]).score(20)
-0.24633160155
>>> fazscore(0.9, [21, 22, 19, 18, 17, 22, 20, 20, 1, 2, 3, 1, 2, 1, 0, 1]).score(20)
1.1069362749
>>> fazscore(0.9, [21, 22, 19, 18, 17, 22, 20, 20, 1, 2, 3, 1, 2, 1, 0, 1]).score(2)
-0.786764452966
>>> fazscore(0.9, [1, 2, 0, 3, 1, 3, 1, 2, 9, 8, 7, 10, 9, 5, 2, 4, 1, 1, 0]).score(9)
1.82262469243
>>> fazscore(0.8, [40] * 200).score(1)
-inf
Actualizar
Como David Kemp señaló correctamente, si se le da una serie de valores constantes y luego se solicita un puntaje z para un valor observado que difiere de los otros valores, el resultado probablemente no sea cero. De hecho, el valor devuelto debe ser infinito. Entonces cambié esta línea,
if self.std() == 0: return 0
a:
if self.std() == 0: return (obs - self.avg) * float("infinity")
Este cambio se refleja en el código de la solución fazscore. Si uno no quiere lidiar con valores infinitos, una solución aceptable podría ser cambiar la línea a:
if self.std() == 0: return obs - self.avg