计算趋势主题或标签的最佳方法是什么?


183

许多网站都提供一些统计信息,例如“过去24小时内最热门的主题”。例如,Topix.com在其“新闻趋势”部分显示了这一点。在这里,您可以看到提及次数增长最快的主题。

我也想为主题计算这样的“嗡嗡声”。我该怎么办?该算法应权衡始终不那么热门的主题。通常(几乎)没有人提及的主题应该是最热门的主题。

Google提供“热门趋势”,topix.com显示“热门主题”,fav.or.it显示“关键字趋势”-所有这些服务都有一个共同点:它们仅向您显示当前异常热门的即将到来的趋势。

诸如“小甜甜布兰妮”,“天气”或“巴黎希尔顿”之类的词不会出现在这些列表中,因为它们总是很热而且很频繁。本文称为“小甜甜布兰妮问题”。

我的问题:如何编码算法或使用现有算法来解决此问题?列出最近24小时内搜索过的关键字,该算法应为您显示10个(例如)最热门的关键字。

我知道,在以上文章中,提到了某种算法。我试图用PHP编写代码,但我认为它不会起作用。它只是找到了大多数,不是吗?

希望您能对我有所帮助(编码示例会很棒)。


3
有趣的问题,好奇地看看人们怎么说。
mmcdole,2009年

14
没有理由关闭,这是一个有效的问题
TStamper

1
这是完全一样的问题,他甚至指出!人们为什么反对它!
达里尔·海因

3
对于您要寻找哪种类型的结果,我有些困惑。该文章似乎表明,在“热门”列表中将始终找到“小甜甜布兰妮”,因为有太多人搜索该词,但是您的问题指出该词不会出现在列表中,因为该词的搜索次数会不会随时间增加太多(它们保持较高水平,但保持稳定)。您要达到哪个结果?“小甜甜布兰妮”应该排名较高还是较低?
e.James 2009年

1
@eJames,“小甜甜布兰妮”不应该排名很高,因为她一直是一个高搜索词,而他正在寻找一个搜索速度很高的词。
mmcdole,2009年

Answers:


103

这个问题需要z分数或标准分数,就像其他人提到的那样,它将考虑历史平均值,而且还要考虑该历史数据的标准差,这使其比仅使用平均值更可靠。

在您的情况下,z得分由以下公式计算,其中趋势将是诸如观看次数/天之类的速率。

z-score = ([current trend] - [average historic trends]) / [standard deviation of historic trends]

当使用z分数时,z分数越高或越低,趋势就越异常,因此,例如,如果z分数为高正,则趋势异常上升,而如果z分数为负,则趋势异常下降。 。因此,一旦您为所有候选趋势计算了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分数最高的主题,标签或页面是当天X的“最新趋势”。最后,用当天的值更新3个字段中的每个字段,明天再重复该过程。

新增加

如上所述的普通z分数未考虑数据的顺序,因此,观测“ 1”或“ 9”的z分数相对于序列[1、1、1、1、1, ,9,9,9,9]。显然,对于趋势发现而言,最新数据应比旧数据具有更大的权重,因此,我们希望“ 1”观测值比“ 9”观测值具有更大的强度得分。为了实现这一点,我提出了一个浮动平均z分数。应该清楚的是,这种方法不能保证在统计上是正确的,但是对于趋势查找或类似方法应该有用。标准z分数和浮动平均值z分数之间的主要区别是使用浮动平均值计算平均人口值和平均人口值的平方。有关详细信息,请参见代码:

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

1
不,您的代码在下一行有一个小错误。$ z_score = $ hits_today-($ average_hits_per_day / $ standard_deviation); 应该是:$ z_score =($ hits_today- $ average_hits_per_day)/ $ standard_deviation; 注意括号中的变化。
Nixuz

1
@nixuz-我缺少什么吗:fazscore(0.8,map(lambda x:40,range(0,200)))。score(1)== 0(对于任何值)?
坎普ͩ

1
@Nixus-以为我可以从坟墓中挖出这个。您可以重新发布此的PHP实现吗?该paste链接似乎没有工作...谢谢!
Drewness

1
对于任何想要的人,我现在都有SQL查询可以做到这一点。
thouliha'2

1
这里的衰减是反直观的。如果您输入2个值,例如[10,20],衰减为0.8,则AVG为10 * 0.8 + 20 * 0.2 = 12。您可能期望值大于15,因为如果衰减,则20的权重应大于10。在numpy.average中使用加权平均值还有更好的选择,您可以在其中创建带有权重的并行列表。例如:data = range(10,30,10)衰减= 0.8 delay_weights = [对于范围内的decay ** a(len(data),0,-1)] print np.average(data,weights = decay_weights)
Jeroen

93

您需要一种可以衡量主题速度的算法-换句话说,如果您对其进行图形显示,则希望显示出令人难以置信的上升趋势。

这是趋势线的一阶导数,将其作为整体计算的加权因子并不难。

归一化

您需要做的一种技术是标准化所有数据。对于您关注的每个主题,请保留一个定义该主题基线的低通滤波器。现在,有关该主题的每个数据点都应进行归一化-减去其基线,您将使所有主题接近于0,且峰值在该行上下。相反,您可能希望将信号除以基线大小,这将使信号达到1.0左右-这不仅使所有信号彼此一致(使基线标准化),而且使尖峰标准化。布兰妮的峰值将比其他人的峰值大,但这并不意味着您应该注意它-峰值相对于她的基线可能很小。

派生

标准化所有内容后,请找出每个主题的斜率。取两个连续的点,并测量差异。正差异呈上升趋势,负差异呈下降趋势。然后,您可以比较归一化的差异,找出与其他主题相比热门的话题,而每个主题的缩放比例均适合其自身的“正常”水平,其顺序可能与其他主题不同。

这确实是解决问题的第一步。您将需要使用更高级的技术(主要是上述方法与其他算法的组合,加权后可以满足您的需求),但这足以使您入门。

关于文章

这篇文章是关于话题趋势的,但不是关于如何计算热点和什么是热点,而是关于如何处理这种算法必须在Lycos和Google之类的地方处理的大量信息。为每个主题提供一个计数器并在进行搜索时找到每个主题的计数器所需的空间和时间非常庞大。本文介绍了在尝试执行此任务时面临的挑战。它确实提到了布兰妮效应,但没有讨论如何克服它。

正如Nixuz所指出的,这也称为Z或标准评分


1
我在编辑之前对此进行了投票,然后回来,我想再次对其进行投票!干得漂亮
mmcdole

谢谢!我会做伪代码,但现在没有时间。也许以后,或者也许其他人会采用这些概念并将其付诸实践……
亚当·戴维斯

非常感谢亚当·戴维斯!如果Nixuz确实描述了相同的内容,那么我认为我已经在PHP中找到了一个解决方案:paste.bradleygill.com/index.php ? paste_id=9206您认为这段代码正确吗?
CAW

这不是话题的加速,而是速度的加速吗?看看最后一个答案
萨普(Sap)

17

查德·伯奇(Chad Birch)和亚当·戴维斯(Adam Davis)是正确的,因为您必须回顾过去才能建立基线。用您的话说,您的问题表明您只想查看过去24小时内的数据,而且运行情况并不理想。

无需查询大量历史数据即可为数据提供一定存储空间的一种方法是使用指数移动平均值。 这样做的好处是您可以每个周期更新一次,然后刷新所有旧数据,因此您只需要记住一个值即可。因此,如果您的期间是一天,则必须为每个主题维护一个“每日平均”属性,您可以通过以下方式进行操作:

a_n = a_(n-1)*b + c_n*(1-b)

哪里a_n是一天的移动平均值n,b是0到1之间的一个常数(越接近1,内存越长),并且c_n是一天的点击次数n。美好的是,如果您在一天结束时执行此更新n,您可以冲洗c_n和删除a_(n-1)

一个警告是,它最初会对您选择的初始值敏感a

编辑

如果因为可以把这种方法,拿n = 5a_0 = 1b = .9

假设新值为5,0,0,1,4:

a_0 = 1
c_1 = 5 : a_1 = .9*1 + .1*5 = 1.4
c_2 = 0 : a_2 = .9*1.4 + .1*0 = 1.26
c_3 = 0 : a_3 = .9*1.26 + .1*0 = 1.134
c_4 = 1 : a_4 = .9*1.134 + .1*1 = 1.1206
c_5 = 4 : a_5 = .9*1.1206 + .1*5 = 1.40854

看起来不是很像平均值吗?请注意,即使我们的下一个输入为5,该值也保持在接近1的水平。这是怎么回事?如果扩大数学范围,您将获得:

a_n = (1-b)*c_n + (1-b)*b*c_(n-1) + (1-b)*b^2*c_(n-2) + ... + (leftover weight)*a_0

剩余重量是什么意思?好吧,在任何平均值中,所有权重都必须加1。如果n为无穷大,并且...可能永远持续下去,则所有权重之和为1。但是,如果n相对较小,那么您将获得大量的权重在原始输入上。

如果您研究以上公式,则应该了解有关此用法的一些知识:

  1. 所有的数据有助于东西平均永远。实际上,在某种程度上,贡献确实很小。
  2. 最新的价值比旧的价值贡献更大。
  3. b越高,新值重要性越小,旧值越重要。但是,b越高,减少a的初始值所需的数据就越多。

我认为前两个特征正是您要寻找的。为了给您一个可以实现的简单方法,这是一个python实现(减去所有数据库交互):

>>> class EMA(object):
...  def __init__(self, base, decay):
...   self.val = base
...   self.decay = decay
...   print self.val
...  def update(self, value):
...   self.val = self.val*self.decay + (1-self.decay)*value
...   print self.val
... 
>>> a = EMA(1, .9)
1
>>> a.update(10)
1.9
>>> a.update(10)
2.71
>>> a.update(10)
3.439
>>> a.update(10)
4.0951
>>> a.update(10)
4.68559
>>> a.update(10)
5.217031
>>> a.update(10)
5.6953279
>>> a.update(10)
6.12579511
>>> a.update(10)
6.513215599
>>> a.update(10)
6.8618940391
>>> a.update(10)
7.17570463519

1
这也被称为无限冲激响应滤波器(IIR)
亚当·戴维斯

嘿,我的答案更好。
约书亚

@Adam真的吗?我对他们不熟悉。是IIR的特例吗?我正在浏览的文章似乎并没有提供在简单情况下可以降低到指数移动平均值的公式。
David Berger,2009年

非常感谢David Berger!如果可行,它将是其他答案的绝佳补充!我有一些问题。我希望您能回答这些问题:1)因素b是否定义了旧数据减肥的速度?2)与仅存储旧数据并计算平均值相比,这种方法是否会给出近似相等的结果?3)这是您的口头表达方式吗?$ AVERAGE_VALUE = $ old_average_value * $ smoothing_factor + $ hits_today *(1- $ smoothing_factor)
CAW

第一点和第三点是正确的。见我的编辑用于位2的细致入微的讨论
大卫·伯格

8

通常使用某种形式的指数/对数衰减机制来找出“嗡嗡声”。对于黑客新闻如何,reddit的,和其他人处理这一个简单的方法的概述,看到这个帖子

这不能完全解决始终流行的问题。您正在寻找的东西似乎类似于Google的“ 热门趋势 ”功能。为此,您可以将当前值除以历史值,然后减去低于某个噪声阈值的值。


是的,Google的热门趋势正是我想要的。历史值应该是多少?例如最近7天的平均值?
caw

1
这取决于数据的不稳定程度。您可以从平均30天开始。如果是周期性事件(例如肯塔基德比),那么每年进行比较可能是有意义的。我将进行实验,看看在实践中最有效的方法。
杰夫·摩泽尔

7

我认为您需要注意的关键词是“异常”。为了确定什么时候是“异常”,您必须知道什么是正常的。也就是说,您将需要历史数据,您可以将其平均以找出特定查询的正常汇率。您可能希望从平均计算中排除异常天数,但这又需要已经有足够的数据,以便您知道要排除哪些天数。

从那里开始,您必须设置一个阈值(我敢肯定,这需要进行实验),如果超出阈值,例如搜索量比正常水平多50%,您可以将其视为“趋势”。或者,如果您希望能够找到您提到的“最流行的X大时尚”,您只需要按偏离正常速度的程度(百分比)订购商品即可。

例如,假设您的历史数据告诉您,小甜甜布兰妮(Britney Spears)通常获得100,000次搜索,而巴黎希尔顿(Paris Hilton)通常获得50,000次。如果您每天都获得比平常多10,000次的搜索量,那么您应该考虑的是巴黎比布兰妮“更热”,因为她的搜索量比平常多20%,而布兰妮的搜索量仅比平常高10%。

天哪,我简直不敢相信我写了一段对比布兰妮·斯皮尔斯和巴黎·希尔顿的“热度”。你对我做了什么?


谢谢,但是仅通过按比例增加它们来订购它们会有点太容易了,不是吗?
CAW

7

我想知道在这种情况下是否有可能使用常规的物理加速度公式?

v2-v1/t or dv/dt

我们可以认为v1是每小时的初始点赞/投票/评论数,而v2是最近24小时的当前每小时“速度”吗?

这更像是一个问题而不是答案,但是似乎可以解决问题。任何具有最高加速度的内容将成为热门话题...

我确定这可能无法解决小甜甜布兰妮的问题:-)


它将起作用,因为它只是计算每次投票/增加的票数,这就是我们所需要的。它可以部分解决“小甜甜布兰妮问题”,因为此搜索词始终很高v1,需要很高v2才能被视为“趋势”。但是,可能有更好,更复杂的公式和算法可以做到这一点。但是,这是一个基本的工作示例。
caw

在您始终需要在“趋势” Feed中添加某些内容的情况下,这是完美的。类似于“浏览”选项卡,您可以在其中列出当前平台上的最佳功能。使用不同的算法,您可能最终会得到空的结果集。
kilianc 2015年

5

一个简单的主题频率梯度可能会起作用-大的正梯度=迅速普及。

最简单的方法是对每天的搜索量进行分类,因此

searches = [ 10, 7, 14, 8, 9, 12, 55, 104, 100 ]

然后找出每天发生的变化:

hot_factor = [ b-a for a, b in zip(searches[:-1], searches[1:]) ]
# hot_factor is [ -3, 7, -6, 1, 3, 43, 49, -4 ]

并应用某种阈值,以便将增加幅度大于50的天视为“热”。如果您愿意,也可以使此操作变得更加复杂。而不是绝对差,您可以采用相对差,这样从100到150会被认为很热,而从1000到1050则不会。或者更复杂的渐变,其中要考虑到一天到第二天之间的趋势。


谢谢。但是我不确切知道什么是梯度以及如何使用它。抱歉!
caw

谢谢。所以我必须建立一个包含每日频率的向量,对吗?我敢肯定,相对值会更好。例子:从100增长到110并不如从1增长到9那样好。但是没有矢量函数可以用来查找最热门的主题吗?仅评估相对值是不够的,是吗?从100增长到200(100%)不如从20,000增长到39,000!
caw

您要将此网站添加到哪种网站?@Autoplectic的建议来计算每天的搜索变化,对于像一个受欢迎的论坛这样的东西来说,这种建议无法很好地扩展,在该论坛中,您有成千上万的主题,每天都在定义新主题。
Quantum7

没错,我需要一种用于处理大量数据,每小时处理数千个主题的算法。
caw

这是一个糟糕的策略。这样一来,有关小甜甜布兰妮(Britney Spears)的搜索总数总共增加了50次,而与欧洲一次新公投的50次搜索一样。
Iman Akbari

4

我曾参与一个项目,目的是从实时Twitter流中找到趋势主题,并对趋势主题进行情感分析(查找趋势主题是否被正面/负面地发现)。我已经使用Storm来处理Twitter流。

我已将报告发布为博客:http : //sayrohan.blogspot.com/2013/06/finding-trending-topics-and-trending.html

我已经使用Total Count和Z-Score进行排名。

我使用的方法有点通用,在讨论部分中,我提到了如何扩展非Twitter应用程序的系统。

希望这些信息能对您有所帮助。


3

如果您仅查看推文或状态消息来获取主题,就会遇到很多麻烦。即使您删除所有停用词。获得更好的主题候选者子集的一种方法是仅关注共享URL的推文/消息,并从那些网页的标题中获取关键字。并确保应用POS标记来获取名词和名词短语。

网页标题通常更具描述性,并包含描述网页内容的字词。此外,共享网页通常与共享突发新闻相关(例如,如果像迈克尔·杰克逊这样的名人去世,那么将会有很多人分享关于他去世的文章)。

我进行了实验,我只从标题中选取流行的关键字,然后获取所有状态消息中这些关键字的总数,这无疑会消除很多噪音。如果以这种方式进行操作,则不需要复杂的算法,只需对关键字频率进行简单排序即可,您已经完成了一半。


2

您可以使用log-likelihood-ratios将当前日期与上个月或上一年进行比较。从统计上讲,这是合理的(假设您的事件不是正态分布的,这可以从您的问题中得出)。

只需按logLR对所有条款进行排序,然后选择前十名。

public static void main(String... args) {
    TermBag today = ...
    TermBag lastYear = ...
    for (String each: today.allTerms()) {
        System.out.println(logLikelihoodRatio(today, lastYear, each) + "\t" + each);
    }
} 

public static double logLikelihoodRatio(TermBag t1, TermBag t2, String term) {
    double k1 = t1.occurrences(term); 
    double k2 = t2.occurrences(term); 
    double n1 = t1.size(); 
    double n2 = t2.size(); 
    double p1 = k1 / n1;
    double p2 = k2 / n2;
    double p = (k1 + k2) / (n1 + n2);
    double logLR = 2*(logL(p1,k1,n1) + logL(p2,k2,n2) - logL(p,k1,n1) - logL(p,k2,n2));
    if (p1 < p2) logLR *= -1;
    return logLR;
}

private static double logL(double p, double k, double n) {
    return (k == 0 ? 0 : k * Math.log(p)) + ((n - k) == 0 ? 0 : (n - k) * Math.log(1 - p));
}

PS,TermBag是单词的无序集合。对于每个文档,您都会创建一揽子条款。只需计算单词的出现次数即可。然后,该方法occurrences返回给定单词的出现次数,并且该方法size返回单词的总数。最好以某种方式对单词进行标准化,通常toLowerCase就足够了。当然,在上面的示例中,您将创建一个包含今天所有查询的文档,并创建一个包含去年所有查询的文档。


抱歉,我不懂代码。什么是TermBags?如果您能很快解释一下此代码的功能,那将是很好的。
caw

1
TermBag是一揽子术语,即该类应该能够回答文本中单词的总数以及每个单词的出现次数。
阿库恩

0

这个想法是要跟踪这些事情,并注意它们与自己的基线相比何时显着跳跃。

因此,对于具有多个阈值的查询,请跟踪每个阈值,并在其变为历史值的某个值(例如几乎翻倍)时进行跟踪,这是一个新的热门趋势。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.