简单的流行度算法
Posted
技术标签:
【中文标题】简单的流行度算法【英文标题】:Simple Popularity Algorithm 【发布时间】:2012-06-23 02:14:06 【问题描述】:总结
正如 Ted Jaspers 明智地指出的那样,我在 2012 年的原始提案中描述的方法实际上是 exponential moving average 的一个特例。这种方法的美妙之处在于它可以递归计算,这意味着您只需为每个对象存储一个流行度值,然后您可以在事件发生时递归地调整该值。无需记录每个事件。
这个单一的流行度值代表所有过去的事件(在所使用的数据类型的限制内),但随着新事件的考虑,旧事件的重要性开始呈指数级降低。该算法将适应不同的时间尺度,并将响应不同的交通量。每次事件发生时,新的人气值可以使用以下公式计算:
(a * t) + ((1 - a) * p)
a
— 介于 0 和 1 之间的系数(值越高,旧事件的折扣越快)
t
— 当前时间戳
p
— 当前流行度值(例如存储在数据库中)
a
的合理值取决于您的应用程序。一个好的起点是a=2/(N+1)
,其中N
是应该显着影响结果的事件数。例如,在事件是页面浏览的低流量网站上,您可能会期望在几天内有数百次页面浏览。选择N=100
(a≈0.02
) 将是一个合理的选择。对于一个高流量的网站,您可能期望在几天内有数百万的页面浏览量,在这种情况下,N=1000000
(a≈0.000002
) 会更合理。 a
的值可能需要随着时间的推移逐渐调整。
为了说明这种流行度算法是多么简单,这里有一个示例,说明如何在 Craft CMS 中用 2 行 Twig 标记实现它:
% set popularity = (0.02 * date().timestamp) + (0.98 * entry.popularity) %
% do entry.setFieldValue("popularity", popularity) %
请注意,无需创建新的数据库表或存储无穷无尽的事件记录来计算流行度。
要记住的一个警告是,指数移动平均线有一个旋转间隔,因此需要几次递归才能认为该值是准确的。这意味着初始条件很重要。例如,如果使用当前时间戳初始化新项目的流行度,则该项目立即成为整个集合中最流行的项目,然后最终稳定到更准确的位置。如果您想推广新内容,这可能是可取的。或者,您可能希望内容从底部向上运行,在这种情况下,您可以使用应用程序首次启动时的时间戳对其进行初始化。您还可以通过使用数据库中所有流行度值的平均值初始化该值来找到一个快乐的媒介,因此它从中间开始。
原提案
有很多 suggested algorithms 用于根据项目的年龄和项目获得的投票、点击或购买次数来计算受欢迎程度。但是,我见过的更强大的方法通常需要过于复杂的计算和多个存储值,这会使数据库变得混乱。我一直在考虑一个非常简单的算法,它不需要存储 任何 变量(除了流行度值本身)并且只需要 一个 简单的计算。这非常简单:
p = (p + t) / 2
这里,p 是存储在数据库中的流行度值,t 是当前时间戳。首次创建项目时,必须初始化 p。有两种可能的初始化方法:
-
用当前时间戳t初始化p
用数据库中所有 p 值的平均值初始化 p
请注意,初始化方法 (1) 使最近添加的项目相对于历史项目具有明显优势,因此添加了一个 相关性 的元素。另一方面,初始化方法 (2) 将新项目与历史项目进行比较。
假设您使用初始化方法 (1) 并使用当前时间戳初始化 p。当项目收到第一次投票时,p 成为创建时间和投票时间的平均值。因此,流行度值 p 仍然代表一个有效的时间戳(假设您四舍五入到最接近的整数),但它代表的实际时间是抽象的。
使用这种方法,只需要一个简单的计算,只需要在数据库中存储一个值(p)。此方法还可以防止值失控,因为给定项目的流行度永远不会超过当前时间。
算法在 1 天内运行的示例:http://jsfiddle.net/q2UCn/ 该算法在 1 年内运行的示例:http://jsfiddle.net/tWU9y/
如果您希望投票以亚秒级的间隔稳定地流入,那么您将需要使用微秒时间戳,例如 php microtime()
函数。否则,标准 UNIX 时间戳将起作用,例如 PHP time()
函数。
现在我的问题是:您认为这种方法有什么重大缺陷吗?
【问题讨论】:
如果您允许人们“不喜欢”项目,这不需要 only 将 p 存储在数据库中。您还必须存储每个“赞”的记录。否则,用户可以一遍又一遍地点赞,然后不喜欢,然后喜欢,然后不喜欢,以增加他们的投票。正如您所说,您只想在收到第一次投票时更改项目的 p 。这意味着您需要跟踪所有投票。 @AlSweigart 好点。该算法可能仅适用于单向投票系统(例如,页面视图是一个正方向的“投票”)。它可能与双向投票系统不太兼容。 【参考方案1】:鉴于其简单性,我认为这是一个非常好的方法。一个非常有趣的结果。
我做了一组快速的计算,发现这个算法似乎确实理解“流行”的含义。它的问题是它明显倾向于支持最近的投票,如下所示:
假设我们将时间分解为从 100 到 1000 的离散时间戳值。假设在 t=100 时,A 和 B(两个项目)具有相同的 P = 100。
A gets voted 7 times on 200, 300, 400, 500, 600, 700 and 800
resulting on a final Pa(800) = 700 (aprox).
B gets voted 4 times on 300, 500, 700 and 900
resulting on a final Pb(900) = 712 (aprox).
当 t=1000 到来时,A 和 B 都收到了投票,所以:
Pa(1000) = 850 with 8 votes
Pb(1000) = 856 with 5 votes
为什么?因为该算法允许一个项目在获得更多最近的选票时快速击败历史领先者(即使该项目的总票数较少)。
编辑包括模拟
OP 创建了一个不错的小提琴,我对其进行了更改以获得以下结果:
http://jsfiddle.net/wBV2c/6/
Item A receives one vote each day from 1970 till 2012 (15339 votes)
Item B receives one vote each month from Jan to Jul 2012 (7 votes)
The result: B is more popular than A.
【讨论】:
很好的分析!您是对的,该算法确实有利于最近的活动,根据应用程序的不同,这可能是可取的,也可能不是可取的。在我看来,这种行为适用于大多数应用程序。即便如此,为易于实施付出的代价很小。 @danielfaraday:请注意,如果您使用 32 位类型,可能会溢出,导致更新大幅暂时降低评级。 @MooingDuck:我不关注。假设 P 会被四舍五入,因此大小将始终等于时间戳的大小(无论时间戳的粒度是秒、毫秒还是微秒)。 daniloquio:经过进一步思考,我不确定这个例子有多大用处。总票数的差异太小,无法得出有效的结论。我将重复我对 btilly 的评论:假设项目 A 是在 1912 年创建的,并且在 2012 年之前每年都会收到一票(总共 100 票)。为了让项目 B 在最后一秒仅以 1 票击败项目 A,它需要在 2009 年之前创建!那是97年后!这是证明:jsfiddle.net/wBV2c。 @danielfaraday 首先,您最好不要在 1970 年之前使用您的算法进行投票,否则负时间戳会给您带来真正的痛苦。你在这里错了,因为这不是一票击败所有人的问题:这完全是关于一个新的热门项目快速击败历史领袖。看看我在答案中的编辑。【参考方案2】:所提出的算法是一种很好的方法,并且是 Exponential Moving Average 的一个特例,其中 alpha=0.5:
p = alpha*p + (1-alpha)*t = 0.5*p + 0.5*t = (p+t)/2 //(for alpha = 0.5)
调整 alpha=0.5 的建议解决方案倾向于支持最近的投票(如 daniloquio 所述)这一事实的一种方法是为 alpha 选择更高的值(例如 0.9 或 0.99)。请注意,将其应用于 daniloquio 提出的测试用例不起作用,因为当 alpha 增加时,算法需要更多的“时间”来解决(因此数组应该更长,这在实际应用中通常是正确的)。
因此:
对于 alpha=0.9,算法会平均最后 10 个值 当 alpha=0.99 时,算法会取最后 100 个值的平均值 对于 alpha=0.999,算法会取最后 1000 个值的平均值 等【讨论】:
很棒的答案!不过有一件事——文章说“更高的 α 会更快地折扣旧的观察结果”,那么 alpha 值不应该与您描述的相反吗?【参考方案3】:我看到了一个问题,只有最后的 ~24 票才算数。
p_i+1 = (p + t) / 2
我们有两票
p2 = (p1 + t2) / 2 = ((p0 + t1) /2 + t2 ) / 2 = p0/4 + t1/4 + t2/2
将其扩展为 32 票给出:
p32 = t*2^-32 + t0*2^-32 + t1*2^-31 + t2*2^-30 + ... + t31*2^-1
所以对于有符号的 32 位值,t0 对结果没有影响。因为 t0 除以 2^32,它对 p32 没有任何贡献。
如果我们有两个项目 A 和 B(无论差异有多大),如果它们都获得相同的 32 票,那么它们将具有相同的受欢迎程度。所以你的历史可以追溯到只有 32 票。如果最后32票相同的话,2032和32票没有区别。
如果差值小于 1 天,则在 17 票后相等。
【讨论】:
这是不正确的。我在这里证明你错了:jsfiddle.net/q2UCn。这是您上面概述的确切分析的实际计算(项目 A 在项目 B 获得 17 票的同一天获得 217 票)。我还在 1 年内对 25 票 (jsfiddle.net/tWU9y) 进行了此分析,得出了类似的结果。 哎呀,你是对的。我没有正确解释结果。修好了。 伊什塔尔:没错。如果两个项目都在恰好相同的时间获得 32 票,则四舍五入将导致它们的流行度值相同。这是证据:jsfiddle.net/c4RVr。但是,这种情况发生的可能性非常很小,除非投票以亚秒级的间隔稳定地流入。在这种情况下,您可以简单地使用微秒时间戳(例如 PHPmicrotime()
函数)。这解决了问题。这是证据:jsfiddle.net/k8HXu。这仅取决于您期望的流量。
@danielfaraday - 即使是一项,在 32 票后,所有历史记录都会丢失。您只记录最后 32 票。这是否是一个问题取决于你。如果你使用微时间(或者说 64 位),你需要更多的投票来清除历史记录,但是,几秒钟后的投票仍然比第 32 次最后投票之前的所有投票更有效。
谢谢伊什塔尔。非常有用的 cmets。【参考方案4】:
缺陷在于,获得 100 票的东西通常比最近只有一张票的东西更有意义。但是,想出一些运行良好的方案变体并不难。
【讨论】:
但最近 1 次投票的时间戳不会按面值计算。取而代之的是,它会平均具有更早的投票。这可能会导致该项目的排名低于获得 100 票的项目(除非所有 100 票都发生在非常很久以前)。 另外,如果 100 票确实发生在 非常 很久以前,流行度的时间相关定义将要求获得 100 票的项目应该 排名低于最近的 1 票。 +1 最后一句话,我同意你假设一些奇怪但不常见的结果是可以接受的。 假设项目 A 是在 1912 年创建的,并且在 2012 年之前每年都会收到一票(总共 100 票)。为了让项目 B 在最后一秒仅以 1 票击败项目 A,它需要在 2009 年之前创建!那是97年后!这是证明:jsfiddle.net/wBV2c。因此,您的评论无效。一个有 100 票的项目将具有更高的流行度值,即使一个有 1 票的项目最近收到了 1 票非常。这实际上证明了这个简单的小算法是多么的健壮。【参考方案5】:我认为上面讨论的逻辑行不通。 p_i+1= (p_i + t) /2
文章 A 在时间戳上被查看:70、80、90 流行度(文章 A):82.5
Article B 在时间戳上被查看:50、60、70、80、90 流行度(Article B):80.625
在这种情况下,B条的热度应该会更高。首先,文章 B 与文章 A 一样最近被查看,其次,文章的浏览次数也比文章 A 多。
【讨论】:
在您的示例中,您会期望 A 和 B 的受欢迎程度等于t → ∞
,这正是发生的情况。但是,考虑到文章 A 比文章 B 更新的初始条件,文章 A 将始终比文章 B 稍有优势。如前所述,该算法偏爱新内容而不是旧内容。我认为这实际上很好地代表了现实生活中的情况——与旧文章相比,新文章往往更相关,所以在所有其他条件相同的情况下,新文章应该会获胜。请记住,此算法的目标不是最好,而是最简单。以上是关于简单的流行度算法的主要内容,如果未能解决你的问题,请参考以下文章