如何推荐下一个成就
简短版本:
我有一个和StackOverflow类似的系统。用户可以获得成就。我有的成就比SO多得多,差不多有1万个,每个用户大约有几百个成就。那么,你会怎么建议用户下一个应该尝试的成就呢?
详细版本:
在django中,我是这样建模的(只展示重要部分):
class User(models.Model):
alias = models.ForeignKey(Alias)
class Alias(models.Model):
achievements = models.ManyToManyField('Achievement', through='Achiever')
class Achievement(models.Model):
points = models.IntegerField()
class Achiever(models.Model):
achievement = models.ForeignKey(Achievement)
alias = models.ForeignKey(Alias)
count = models.IntegerField(default=1)
我的算法是找到每个和当前登录用户有共同成就的其他用户,然后查看他们的所有成就,并按出现次数排序:
def recommended(request) :
user = request.user.get_profile()
// The final response
r = {}
// Get all the achievements the user's aliases have received
// in a set so they aren't double counted
achievements = set()
for alias in user.alias_set.select_related('achievements').all() :
achievements.update(alias.achievements.all())
// Find all other aliases that have gotten at least one of the same
// same achievements as the user
otherAliases = set()
for ach in achievements :
otherAliases.update(ach.alias_set.all())
// Find other achievements the other users have gotten in addition to
// the shared ones.
// And count the number of times each achievement appears
for otherAlias in otherAliases :
for otherAch in otherAlias.achievements.all() :
r[otherAch] = r.get(otherAch, 0) + 1
// Remove all the achievements that the user has already gotten
for ach in achievements :
r.pop(ach)
// Sort by number of times the achievements have been received
r = sorted(r.items(), lambda x, y: cmp(x[1], y[1]), reverse=True)
// Put in the template for showing on the screen
template_values = {}
template_values['achievements'] = r
但是这个过程运行得非常慢,而且总是返回整个列表,其实用户只需要前几个最重要的成就来追求。
所以,我欢迎大家推荐其他算法和/或代码改进的方法。如果你能给我一个好的推荐算法,我会在我的系统中给你一个成就奖励 :)
2 个回答
我建议你把前面提到的三个步骤(成就、其他别名、计数)合并成一个SQL语句。现在你发出了很多查询请求,然后在Python中处理成千上万的行数据,这个工作其实应该交给数据库来做。比如下面这段代码
for otherAlias in otherAliases : #For every single other user
for otherAch in otherAlias.achievements.all() : #execute a query
r[otherAch] = r.get(otherAch, 0) + 1
会发出成千上万的复杂查询。
相反,你可以通过SQL来完成这个任务,方法是将“Achiever”表与自身连接,条件是别名ID不同而成就ID相同。然后你可以根据成就ID进行分组,并计算数量。
在下面的查询中,表“B”是其他用户的成就,而“Achiever”是我们自己的成就。如果其他用户分享了某个成就,他们在“B”中会为每个分享的成就出现一次。接着,我们根据别名ID进行分组,并计算他们出现的次数,这样你就能得到一个包含ID和计数的整洁表格。
这段代码非常粗略(这里没有SQL可用)
SELECT B.Alias_id, COUNT(B.achievement_id)
FROM Achiever, Achiever as B
WHERE Achiever.achievement_id == B.achievement_id
AND Achiever.Alias_id == <insert current user alias here>;
GROUP BY B.Alias_id
如果这个方法按我想的那样工作,你将得到一个包含其他用户别名的表格,以及他们与当前用户共享的成就数量。
接下来,你要写一个SQL语句,使用上面的查询作为“内部选择”——可以叫它用户。然后将这个结果与当前用户的成就表和Achiever表连接。你可能只想关注与当前用户相似的前10个用户。
我现在没时间写一个好的查询,但你可以看看JOIN语句,连接这10个用户和当前用户之间的成就ID——如果不存在,就把这个ID设置为NULL。然后只筛选出那些结果为NULL的行(未完成的成就)。
一种推荐成就的方法是看看有多少用户已经获得了这些成就,然后推荐那些受欢迎的成就。当用户完成了这些成就后,再逐渐推荐那些稍微不那么受欢迎的。不过,这种方法有个简单的假设,就是大家都想追求热门成就。这可能会导致热门成就变得更热门,而不那么受欢迎的成就就会被忽视。好在这种方法不需要太多资源,运行起来也很快。(只需保持一个成就列表和每个成就被完成的次数)
另一种方法是使用一些机器学习算法,试图根据用户已经获得的成就来猜测他们可能会追求哪些成就。我觉得k最近邻算法在这里会表现得不错。你可以设定一个阈值,只输出高于这个阈值的成就。现在,我不确定这种方法是否比你现在的方案更快,但你可以在用户获得新成就时运行推荐系统,保存前五个推荐,然后在需要推荐时再把这些推荐给用户。
希望这些对你有帮助。=)