谈谈推荐排序
本文说的排序并不是指「冒泡」之类的技术概念,而是一个业务相关的问题。
举例来说:某个网站,每天都能产生很多数据,需要一个推荐列表页面来展示数据。最初是完全按照时间倒序来排序的,但是这样就产生了一个问题:新鲜的数据不一定是有价值的数据!假设某个时段灌水的数据比较多,那么用户当时在列表页看到的就都是灌水的内容。既然如此,不妨换个思路:给每个数据投票,投票规则可以按业务逻辑自定义,比如:每次评论加一票,每次转发加两票等等。然后按照投票数来倒序是不是就可以了?可惜还有问题:有价值的数据不一定是新鲜的数据!假设历史上曾经产生了一个高票的数据,那么不管过多久,它都会一直占据前排座位。由此可见最好的结果是让用户能看到既新鲜又有价值的数据。
实际上此问题早就有人讨论过了,建议大家阅读相关文章:
榜单类应用我所喜欢的算法 Social Media排序算法的四种模式 Hacker News与Reddit的算法比较 Reddit, Stumbleupon, Del.icio.us and Hacker News Algorithms Exposed!一种方法是让分数随着时间的流逝而降低,比如说一条数据原本的分数是 100 分,第二天衰减到 50 分,第三天衰减到 25 分,以此类推。此方法的优点是简单易懂,缺点是数据的分数随着时间的变化而变化,实际应用中,可能需要用 cron 之类的方法定期更新数据的分数,一旦数据比较多就悲剧了。
另一种方法是让分数本身就包含时间因子:「t / 43200 + log10(x)」:
「t」表示的是数据生成时的时间戳。 「43200」表示的是半天(12 个小时)的秒数。 「x」表示的是数据得到的票数。于是乎可知「t / 43200」得到的就是有多少个半天:
<?php $result = []; $dates = [ '2017-01-01', '2017-01-02', '2017-01-03', '2017-01-04', '2017-01-05', ]; foreach ($dates as $date) { $result[$date] = strtotime($date) / 43200; } var_export($result); /* 结果: array ( '2017-01-01' => 34334, '2017-01-02' => 34336, '2017-01-03' => 34338, '2017-01-04' => 34340, '2017-01-05' => 34342, ) */ ?>
实际上「t / 43200」相当于是「时间分」,随着时间的增加而增加,其基数比较大,保证了新鲜的数据会排在前面,同时每天「时间分」的差值为 2,后面会用到这个数字。
那「log10(x)」又是什么意思呢,我们可以把它理解成「价值分」,其基数比较小,之所以做 log10 的对数运算就是为了快速衰减,确保前面投票的权重大于后面投票,如此有利于让有价值的数据尽早被发现:
<?php $result = []; $values = [ 10000, 1000, 100, ]; foreach ($values as $value) { $result[$value] = sprintf('%.5f', log10($value)); } var_export($result); /* 结果: array ( 10000 => '4.00000', 1000 => '3.00000', 100 => '2.00000', ) */ ?>
如上所示:如果一条数据的得票数是 100,那么它的价值分将是 2,最多可以在推荐列表上停留一天;如果一条数据的得票数是 1000,那么它的价值分将是 3,最多可以在推荐列表上停留一天半;如果一条数据的得票数是 10000,那么它的价值分将是 4,最多可以在推荐列表上停留两天,如此保证价值分高的数据不会霸占前排座位太久。
最后要说明的是,与其死记硬背公式,倒不如理解它的意思,并根据客观情况调整,比如说,有的人觉得时间分本身太大了,那么我们可以减去一个参照时间。有的人觉得为什么一定要除以 12 个小时,除以 12.5 个小时可不可以?答案当然是可以的!有的人觉得为什么一定要用以 10 为底的对数,可不可以使用科学对数?答案当然也是可以的!只要你理解了分数中「时间分」和「价值分」的用意,那么怎么实现它们都可以。
文章来源:
Author:老王
link:https://huoding.com/2017/04/25/617