这个问题需要一个 z 分数或标准分数,它会考虑到历史平均值,正如其他人提到的那样,还要考虑这个历史数据的标准偏差,使其比仅使用平均值更稳健。
在您的情况下,z 分数是通过以下公式计算的,其中趋势将是诸如观看次数/天之类的比率。
z-score = ([current trend] - [average historic trends]) / [standard deviation of historic trends]
当使用z-score时,z-score越高或越低,趋势越异常,例如,如果z-score为高正则趋势异常上升,而如果z-score为高负则异常下降. 因此,一旦您计算了所有候选趋势的 z 分数,最高的 10 个 z 分数将与最异常增加的 z 分数相关。
有关 z 分数的更多信息,请参阅Wikipedia。
代码
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
样本输出
>>> 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
笔记
如果您不想考虑太多历史记录,您可以将此方法与滑动窗口(即过去 30 天)一起使用,这将使短期趋势更加明显,并可以减少处理时间。
您还可以使用 z 分数来表示从一天到第二天的观看次数变化等值,以定位每天增加/减少观看次数的异常值。这就像使用每日视图的斜率或导数一样。
如果您跟踪人口的当前规模、人口的当前总数以及人口的当前总数 x^2,则无需重新计算这些值,只需更新它们,因此您只需将这些值保留为历史记录,而不是每个数据值。下面的代码演示了这一点。
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()
使用这种方法,您的工作流程将如下所示。为每个主题、标签或页面创建一个浮点字段,用于显示数据库中的总天数、查看次数总和以及查看次数平方和。如果您有历史数据,请使用该数据初始化这些字段,否则初始化为零。在每天结束时,使用当天的查看次数与存储在三个数据库字段中的历史数据计算 z 分数。具有最高 X z 分数的主题、标签或页面是您当天的 X 个“最热门趋势”。最后用当天的值更新 3 个字段中的每一个,并在第二天重复该过程。
新增内容
上面讨论的正常 z 分数不考虑数据的顺序,因此观察“1”或“9”的 z 分数与序列 [1, 1, 1, 1 , 9, 9, 9, 9]。显然,对于趋势发现,最新数据应该比旧数据具有更大的权重,因此我们希望“1”观察值比“9”观察值具有更大的量值分数。为了实现这一点,我提出了一个浮动平均 z 分数。应该清楚的是,这种方法不能保证在统计上是合理的,但应该对趋势发现或类似方法有用。标准 z-score 和浮动平均 z-score 的主要区别在于使用浮动平均数来计算平均人口值和平均人口值的平方。详情见代码:
代码
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
>>> 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
更新
正如 David Kemp 正确指出的那样,如果给定一系列常数值,然后请求与其他值不同的观察值的 zscore,则结果可能应该是非零的。实际上返回的值应该是无穷大。所以我改变了这条线,
if self.std() == 0: return 0
至:
if self.std() == 0: return (obs - self.avg) * float("infinity")
此更改反映在 fazscore 解决方案代码中。如果不想处理无限值,则可接受的解决方案是将行改为:
if self.std() == 0: return obs - self.avg