什么是计算热门话题或标签的最佳方法?

219

许多网站提供一些统计数据,比如“过去24小时最热门的话题”。例如,Topix.com在它的“新闻趋势”栏目中展示了这一点。在那里,你可以看到提及数量增长最快的话题。

我也想计算一个话题的“热度”。我该怎么做呢?算法应该减小总是热门的话题的权重。通常(几乎)没有人提及的话题应该是最热门的。

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

像“布兰妮·斯皮尔斯”、“天气”或“帕丽斯·希尔顿”之类的术语不会出现在这些列表中,因为它们总是很热门和频繁。 这篇文章称之为“布兰妮·斯皮尔斯问题”。

我的问题是:你如何编写一个算法或使用现有的算法来解决这个问题?有了过去24小时搜索关键字列表,算法应该向你展示最热门的10个(例如)。

我知道,在上面的文章中,有某种算法的提到。 我试着用PHP编写它,但我不认为它会起作用。它只找到了多数派,是吗?

希望您能帮助我(编写示例代码将是很好的)。


1
这完全是同样的问题,他甚至都说了!为什么人们还要点赞它! - Darryl Hein
3
我有点困惑你想要哪种结果。文章似乎表明,“布兰妮·斯皮尔斯”将始终出现在“热门”列表中,因为很多人搜索该术语,但是你的问题则指出,它不会出现在列表中,因为随着时间的推移,该术语的搜索量并没有增加很多(尽管仍然很高且稳定)。你想要什么样的结果?“布兰妮·斯皮尔斯”排名高还是低? - e.James
1
@eJames,"Britney Spears" 不应该排名靠前,因为她一直是一个高搜索量的搜索词,而他正在寻找具有高速度的搜索词。 - mmcdole
1
重新开放投票:这是对原问题的跟进问题,询问在尝试解决原问题时出现的特殊问题。 - Fabian Steeg
1
这个问题不是关于精确复制或近似复制的,而是关于使用特定算法解决特定问题的。 - thomasrutter
显示剩余14条评论
11个回答

127
这个问题需要使用z分数或标准分数,它将考虑到历史平均值,正如其他人已经提到的那样,但也会考虑到历史数据的标准差,使其比仅使用平均值更加强大。
在您的情况下,z分数是通过以下公式计算的,其中趋势将是一个速率,例如每天的浏览量。
z-score = ([current trend] - [average historic trends]) / [standard deviation of historic trends]

当使用z得分时,z得分越高或越低,趋势就越不正常。例如,如果z得分非常正向,则趋势异常上升,而如果它非常负向,则趋势异常下降。因此,一旦计算出所有候选趋势的z得分,最高的10个z得分将与最不正常的增长z得分相关。
有关z得分的更多信息,请参见维基百科代码
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

笔记

  • You can use this method with a sliding window (i.e. last 30 days) if you wish not to take to much history into account, which will make short term trends more pronounced and can cut down on the processing time.

  • You could also use a z-score for values such as change in views from one day to next day to locate the abnormal values for increasing/decreasing views per day. This is like using the slope or derivative of the views per day graph.

  • If you keep track of the current size of the population, the current total of the population, and the current total of x^2 of the population, you don't need to recalculate these values, only update them and hence you only need to keep these values for the history, not each data value. The following code demonstrates this.

      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()
    
  • Using this method your work flow would be as follows. For each topic, tag, or page create a floating point field, for the total number of days, sum of views, and sum of views squared in your database. If you have historic data, initialize these fields using that data, otherwise initialize to zero. At the end of each day, calculate the z-score using the day's number of views against the historic data stored in the three database fields. The topics, tags, or pages, with the highest X z-scores are your X "hotest trends" of the day. Finally update each of the 3 fields with the day's value and repeat the process next day.

新增内容

如上所述,普通的z分数不考虑数据的顺序,因此对于序列[1, 1, 1, 1, 9, 9, 9, 9]中的观察值'1'或'9',其z分数具有相同的大小。显然,在趋势发现中,最新的数据应该比旧的数据更有权重,因此我们希望'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()

样例输入输出

>>> 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所指出的那样,如果给定一系列常数值,然后请求一个与其他值不同的观测值的z分数,则结果应该是非零值。事实上,返回的值应该是无穷大。因此我更改了这一行:

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(对于任何值)? - kͩeͣmͮpͥ ͩ
1
@Nixus - 我想从坟墓里挖出这个问题。你能重新发布一下这个PHP实现吗?'paste'链接似乎无法使用...谢谢! - Drewness
1
这里的衰减是反直觉的;如果您输入两个值,比如[10, 20],衰减为0.8,则平均值为100.8+200.2=12。如果有衰减,您会期望一个超过15的值,因为20应该比10更重要。使用numpy.average中的加权平均值有一个更好的替代方案,其中您可以创建一个带有权重的并行列表。例如:data=range(10,30,10) decay=0.8 decay_weights = [decay**a for a in range(len(data),0,-1)] print np.average(data,weights=decay_weights) - Jeroen
2
使用适合您数据的分布是最好的选择。正态分布的数据只是一种假设,但您应该根据您的用例来衡量它。 - Nixuz
显示剩余16条评论

94
你需要一个算法来测量话题的速度,换句话说,如果你将其绘制成图形,你想要显示那些以惊人的速度上升的话题。
这是趋势线的一阶导数,将其作为加权因子纳入到您的整体计算中并不难。
归一化
你需要做的一个技巧是归一化所有数据。对于你正在跟踪的每个话题,保持一个非常低通滤波器,定义该话题的基线。现在,关于该话题的每个数据点都应该被归一化 - 减去它的基线,你会得到所有话题接近0的值,并且有上下波动。你可能希望将信号除以其基线幅度,这将使信号达到约1.0 - 这不仅会使所有信号与彼此保持一致(归一化基线),而且还会使尖峰归一化。布兰妮的尖峰会比其他人的尖峰大几个数量级,但这并不意味着你应该关注它 - 相对于她的基线,尖峰可能非常小。
导出
一旦你归一化了所有内容,就可以找出每个话题的斜率。取两个连续点,然后测量它们之间的差异。正差是上升趋势,负差是下降趋势。然后,你可以比较归一化的差异,并找出相对于其他话题而言在人气方面急剧上升的话题 - 每个话题都要适当地缩放到其自己的“正常值”,这可能与其他话题相比有数量级的不同。
这实际上是解决问题的第一步。还有更高级的技术需要使用(主要是将上述技术与其他算法相结合,加权以适应您的需求),但这应该足以让您入门。
关于这篇文章
该文章讨论了热门话题,但不是关于如何计算什么是热门的,而是关于如何处理像Lycos和Google这样的地方必须处理的大量信息。为每个话题提供计数器以及在搜索它时找到每个话题的计数器所需的空间和时间非常巨大。该文章讨论了尝试完成此任务时面临的挑战。它提到了Brittney效应,但没有讨论如何克服它。
正如Nixuz指出,这也被称为Z分数或标准分数

谢谢!我本来想写伪代码的,但现在没时间。也许以后会有时间,或者其他人可以拿这些概念来实现它... - Adam Davis
非常感谢你,Adam Davis!如果Nixuz描述的是同样的问题,我认为我已经在PHP中找到了一个解决方案:http://paste.bradleygill.com/index.php?paste_id=9206 你认为这段代码正确吗? - caw
应该是主题加速而不是速度加速吧?请查看最后一个答案。 - Sap

18

Chad Birch 和 Adam Davis 是正确的,你需要向后查看以建立一个基准。从你的问题中可以看出,你只想查看过去 24 小时的数据,这样是不太可行的。

一种不必查询大量历史数据就能让你的数据有记忆的方法是使用指数移动平均。其优点是你可以每个时间段更新一次,然后清除所有旧数据,因此你只需要记住一个值。例如如果你的时间段是一天,那么你需要为每个主题维护一个“每日平均”属性,你可以通过以下方式实现:

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

当天数为n时,a_n为移动平均值,c_n是在第n天的点击次数,b是介于0和1之间的某个常数(越接近1,记忆时间越长)。这种方法的优点在于,在第n天结束时执行此更新后,可以清除c_na_(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 Davis
1
非常感谢您,David Berger!如果它有效,那将是其他答案的很好补充!不过我有一些问题,希望您能回答: 1)因子b是否定义了旧数据失去权重的速度? 2)与仅存储旧数据并计算平均值相比,这种方法是否会给出大致相等的结果? 3)这是您的公式吗?$average_value = $old_average_value * $smoothing_factor + $hits_today * (1-$smoothing_factor) - caw
1
也许我错过了什么,但我无法理解如何合理地使用移动平均来解决这个问题。一旦你已经计算出趋势的移动平均值,你怎么知道哪个趋势相对于其他趋势上升得最快?你能否添加更多关于如何解决最初提出的问题的信息。谢谢。 - Nixuz
@Nixuz,你能告诉我怎么做吗? - Jesvin Jose
完整的代码和解释都在我的回答中。 - Nixuz
显示剩余5条评论

10

通常,“buzz”是通过某种指数/对数衰减机制来计算的。有关Hacker News,Reddit和其他网站如何以简单的方式处理此问题的概述,请参见这篇文章

这并不能完全解决总是受欢迎的事物。你正在寻找的似乎是像谷歌的“热门趋势”功能一样的东西。为此,您可以将当前值除以历史值,然后减去低于某个噪声阈值的值。


是的,Google的热门趋势正是我所需要的。历史价值应该是多少呢?例如,过去7天的平均值? - caw
1
这取决于你的数据有多易变。你可以从30天平均值开始。如果它是一个周期性的事情(例如肯塔基德比赛),那么进行年度比较可能是有意义的。我建议尝试一下,看看在实践中哪种方法最有效。 - Jeff Moser

9
我认为你需要注意的关键词是“异常”。为了确定什么是“异常”,你需要知道什么是正常的。也就是说,你需要历史数据,可以对其进行平均处理,以找出特定查询的正常率。您可能希望从平均计算中排除异常日期,但这又需要足够的数据,以便您知道应该排除哪些日期。
从那里开始,您将不得不设置一个阈值(这需要进行实验,我相信),如果某些内容超出了阈值,例如比正常情况多50%的搜索量,则可以将其视为“趋势”。或者,如果您想能够找到“最热门的X”,就只需要按它们与正常速率的百分比相差多少来排序。
例如,假设您的历史数据告诉您,布兰妮·斯皮尔斯通常会获得100,000次搜索,而帕丽斯·希尔顿通常会获得50,000次搜索。如果有一天他们都比正常情况多获得10,000次搜索,您应该认为帕丽斯比布兰妮更“热门”,因为她的搜索量增加了20%,而布兰妮仅增加了10%。
天啊,我不能相信我刚刚写了一段比较布兰妮·斯皮尔斯和帕丽斯·希尔顿“热度”的段落。你对我做了什么?

谢谢,但仅按其百分比增长来订购它们会有点太简单了,不是吗? - caw

7

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

v2-v1/t or dv/dt

我们可以将v1视为每小时的初始点赞/投票/评论计数,将v2视为过去24小时内每小时的当前“速度”?
这更像是一个问题而不是答案,但似乎它可能会起作用。任何具有最高加速度的内容都将成为热门话题...
我相信这可能无法解决布兰妮·斯皮尔斯的问题 :-)

它会起作用,因为它只是计算每个时间段的投票/点赞增量,这正是我们所需要的。它可以在某种程度上解决“布兰妮·斯皮尔斯问题”,因为这个搜索词总是具有很高的v1,需要非常高的v2才能被认为是“趋势”。然而,可能有更好和更复杂的公式和算法来实现这一点。尽管如此,这是一个基本的工作示例。 - caw
在你需要始终在“热门”动态中拥有一些内容的情况下,这是完美的。就像一个“探索”选项卡,你可以列出平台上现在最好的东西。使用不同的算法,你可能会得到一个空的结果集。 - kilianc

6

可能一个简单的主题频率渐变就可以起到作用——大的正渐变=快速增长的受欢迎程度。

最简单的方法是将每天的搜索次数分组,这样你就有了类似于:

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
我建议使用b/a而不是b-a,因为即使热门话题有微小的变化,也会导致巨大的绝对差异。 - DollarAkshay

4
我曾经参与了一个项目,我的目标是从实时Twitter流中找到热门话题,并对这些热门话题进行情感分析(找出热门话题是否有积极/消极的讨论)。我使用Storm来处理Twitter数据流。我已将报告作为博客发布:http://sayrohan.blogspot.com/2013/06/finding-trending-topics-and-trending.html 我使用总数和Z-Score进行排名。我所采用的方法有些通用,在讨论部分中,我提到了如何将该系统扩展到非Twitter应用程序中。希望这些信息能够帮助到您。

3

您可以使用对数似然比来比较当前日期与上个月或去年的数据。这在统计学上是合理的(假设您的事件不是正态分布的,这是从您的问题中可以推断出来的)。

只需按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是一个词袋,即该类应能够回答文本中单词的总数以及每个单词的出现次数。 - akuhn
感谢@akuhn的解释和代码片段。我将其移植到JavaScript中并且它可以工作。我正在尝试理解输出:在某些情况下,我看到负值(例如-4.679577112488872 AAPL),而在其他情况下,是正值(例如3.4914628235919807 CRWD)。这个想法是最高价值是趋势吗?负值代表什么? - titusmagnus

3
如果你仅仅查看推特或状态消息来获取话题,那么你将遇到很多噪音,即使你移除所有停用词。获得更好的话题候选子集的一种方法是只关注分享URL的推特/消息,并从这些网页标题中获取关键词。并确保应用POS标记以获取名词和名词短语。网页标题通常更具描述性,并包含描述页面内容的单词。此外,共享网页通常与共享突发新闻相关(例如,如果像迈克尔·杰克逊这样的名人去世,则会有很多人分享有关他死亡的文章)。
我进行了实验,只从标题中提取流行关键字,然后获取这些关键字在所有状态消息中的总计数,它们可以消除很多噪音。如果您按此方式操作,则不需要复杂算法,只需对关键字频率进行简单排序即可完成一半的工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接