Esse problema exige uma pontuação z ou pontuação padrão, que levará em consideração a média histórica, como outras pessoas mencionaram, mas também o desvio padrão desses dados históricos, tornando-o mais robusto do que apenas usando a média.
No seu caso, um escore z é calculado pela fórmula a seguir, onde a tendência seria uma taxa como visualizações / dia.
z-score = ([current trend] - [average historic trends]) / [standard deviation of historic trends]
Quando um escore z é usado, quanto maior ou menor o escore z, mais anormal é a tendência; por exemplo, se o escore z é altamente positivo, a tendência aumenta anormalmente, enquanto se é altamente negativo, diminui anormalmente. . Portanto, depois de calcular o escore z para todas as tendências candidatas, os 10 escores z mais altos se relacionam com os escores z que aumentam anormalmente.
Por favor, consulte a Wikipedia para obter mais informações sobre z-scores.
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
Saída de amostra
>>> 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
Você pode usar esse método com uma janela deslizante (ou seja, nos últimos 30 dias) se não levar muito em consideração o histórico, o que tornará as tendências de curto prazo mais acentuadas e reduzirá o tempo de processamento.
Você também pode usar um z-score para valores como alteração de visualizações de um dia para o dia seguinte para localizar os valores anormais de aumento / diminuição de visualizações por dia. É como usar a inclinação ou a derivada das visualizações por dia.
Se você acompanhar o tamanho atual da população, o total atual da população e o total atual de x ^ 2 da população, não precisará recalcular esses valores, apenas atualizá-los e, portanto, precisará apenas mantenha esses valores para o histórico, não cada valor de dados. O código a seguir demonstra isso.
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()
Usando esse método, seu fluxo de trabalho seria o seguinte. Para cada tópico, tag ou página, crie um campo de ponto flutuante, para o número total de dias, soma de visualizações e soma de visualizações ao quadrado no banco de dados. Se você tiver dados históricos, inicialize esses campos usando esses dados; caso contrário, inicialize com zero. No final de cada dia, calcule a pontuação z usando o número de visualizações do dia em relação aos dados históricos armazenados nos três campos do banco de dados. Os tópicos, tags ou páginas com as maiores pontuações z de X são as suas "tendências mais quentes" do dia. Por fim, atualize cada um dos 3 campos com o valor do dia e repita o processo amanhã.
Nova adição
Os escores z normais, conforme discutido acima, não levam em consideração a ordem dos dados e, portanto, o escore z para uma observação de '1' ou '9' teria a mesma magnitude em relação à sequência [1, 1, 1, 1 9, 9, 9, 9]. Obviamente, para encontrar tendências, os dados mais atuais devem ter mais peso que os dados mais antigos e, portanto, queremos que a observação '1' tenha uma pontuação de magnitude maior que a observação '9'. Para isso, proponho um escore z médio flutuante. Deve ficar claro que esse método NÃO é estatisticamente correto, mas deve ser útil para encontrar tendências ou algo semelhante. A principal diferença entre o escore z padrão e o escore z médio flutuante é o uso de uma média flutuante para calcular o valor médio da população e o valor médio da população ao quadrado. Consulte o código para obter detalhes:
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()
IO de amostra
>>> 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
Atualizar
Como David Kemp apontou corretamente, se for fornecida uma série de valores constantes e, em seguida, for solicitado um zscore para um valor observado diferente dos outros valores, o resultado provavelmente deve ser diferente de zero. De fato, o valor retornado deve ser infinito. Então eu mudei essa linha,
if self.std() == 0: return 0
para:
if self.std() == 0: return (obs - self.avg) * float("infinity")
Essa alteração é refletida no código da solução fazscore. Se alguém não quiser lidar com valores infinitos, uma solução aceitável seria alterar a linha para:
if self.std() == 0: return obs - self.avg