在Django中实现流行度算法

时间:2009-12-27 06:13:48

标签: python django algorithm postgresql

我正在创建一个类似于reddit和黑客新闻的网站,其中包含链接和投票数据库。我正在实施黑客新闻的流行算法,事情正在顺利进行,直到实际收集这些链接并显示它们。算法很简单:

Y Combinator's Hacker News:
Popularity = (p - 1) / (t + 2)^1.5`

Votes divided by age factor.
Where`

p : votes (points) from users.
t : time since submission in hours.

p is subtracted by 1 to negate submitter's vote.
Age factor is (time since submission in hours plus two) to the power of 1.5.factor is (time since submission in hours plus two) to the power of 1.5.

我在yonder Complex ordering in Django上提出了一个非常类似的问题,但我没有考虑我的选择,而是选择了一个并尝试使其工作,因为我是用PHP / MySQL做的,但我现在知道Django做了很多事情不同。

我的模特看起来像这样(确切地说)

class Link(models.Model):
category = models.ForeignKey(Category)
user = models.ForeignKey(User)
created = models.DateTimeField(auto_now_add = True)
modified = models.DateTimeField(auto_now = True)
fame = models.PositiveIntegerField(default = 1)
title = models.CharField(max_length = 256)
url = models.URLField(max_length = 2048)

def __unicode__(self):
    return self.title

class Vote(models.Model):
link = models.ForeignKey(Link)
user = models.ForeignKey(User)
created = models.DateTimeField(auto_now_add = True)
modified = models.DateTimeField(auto_now = True)
karma_delta = models.SmallIntegerField()

def __unicode__(self):
    return str(self.karma_delta)

和我的观点:

def index(request):
popular_links = Link.objects.select_related().annotate(karma_total = Sum('vote__karma_delta'))
return render_to_response('links/index.html', {'links': popular_links})

现在从我之前的问题开始,我正在尝试使用排序功能来实现该算法。这个问题的答案似乎认为我应该把算法放在select和sort中。我打算对这些结果进行分页,所以我不认为我可以在没有抓住所有内容的情况下在python中进行排序。关于如何有效地做到这一点的任何建议?

修改

这还没有奏效,但我认为这是朝着正确方向迈出的一步:

from django.shortcuts import render_to_response
from linkett.apps.links.models import *

def index(request):
popular_links = Link.objects.select_related()
popular_links = popular_links.extra(
    select = {
        'karma_total': 'SUM(vote.karma_delta)',
        'popularity': '(karma_total - 1) / POW(2, 1.5)',
    },
    order_by = ['-popularity']
)
return render_to_response('links/index.html', {'links': popular_links})

这会导致错误:

Caught an exception while rendering: column "karma_total" does not exist
LINE 1: SELECT ((karma_total - 1) / POW(2, 1.5)) AS "popularity", (S...

编辑2

更好的错误?

TemplateSyntaxError: Caught an exception while rendering: missing FROM-clause entry for table "vote"
LINE 1: SELECT ((vote.karma_total - 1) / POW(2, 1.5)) AS "popularity...

我的index.html只是:

{% block content %}

{% for link in links %}
 
  
   karma-up
   {{ link.karma_total }}
   karma-down
  
  {{ link.title }}
  Posted by {{ link.user }} to {{ link.category }} at {{ link.created }}

{% empty %} No Links {% endfor %} {% endblock content %}

编辑3 非常接近!同样,所有这些答案都很棒,但我专注于一个特定的答案,因为我认为它最适合我的情况。

from django.db.models import Sum
from django.shortcuts import render_to_response
from linkett.apps.links.models import *

def index(request): popular_links = Link.objects.select_related().extra( select = { 'popularity': '(SUM(links_vote.karma_delta) - 1) / POW(2, 1.5)', }, tables = ['links_link', 'links_vote'], order_by = ['-popularity'], ) return render_to_response('links/test.html', {'links': popular_links})

运行这个我的错误是因为我缺乏按值分组。具体做法是:

TemplateSyntaxError at /
Caught an exception while rendering: column "links_link.id" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: ...karma_delta) - 1) / POW(2, 1.5)) AS "popularity", "links_lin...

我不确定为什么我的links_link.id不会出现在我的小组中,但我不知道如何更改我的小组,django通常会这样做。

4 个答案:

答案 0 :(得分:9)

在黑客新闻中,只有210个最新故事和210个最受欢迎的故事被分页(7页,每页30个故事)。我的猜测是限制的原因(至少部分)是这个问题。

为什么不放弃所有最流行的故事的SQL,而只是保留一个运行列表呢?一旦你建立了前210个故事的列表,你只需要担心在新的投票进入时重新排序,因为相对的订单会随着时间的推移而保持。当新的投票确实进入时,你只需要担心重新安排接受投票的故事。

如果收到投票的故事不在列表中,请计算该故事的分数,以及列表中 的最不受欢迎的故事。如果收到投票的故事较低,那么你就完成了。如果它更高,则计算第二个到最不受欢迎的故事(故事209)的当前分数并再次进行比较。继续努力,直到找到一个得分较高的故事,然后将新投票的故事放在排名的正下方。当然,除非它达到#1。

这种方法的好处在于它限制了您必须查看的故事集,以找出最佳故事列表。在绝对最坏的情况下,您必须计算211个故事的排名。所以它非常有效,除非您必须从现有数据集建立列表 - 但这只是一次性的惩罚,假设您在某个地方缓存列表。

Downvotes是另一个问题,但我只能投票(无论如何,在我的业力水平)。

答案 1 :(得分:4)

popular_links = Link.objects.select_related()
popular_links = popular_links.extra(
    select = {
        'karma_total': 'SUM(vote.karma_delta)',
        'popularity': '(karma_total - 1) / POW(2, 1.5)'
    },
    order_by = ['-popularity']
)

或者选择一些理智的数字,以任何你喜欢的方式使用python对选择进行排序,并缓存它是否对所有用户来说都是静态的 - 看起来会是这样 - 将缓存到期时间设置为一分钟左右。

但是extra在高度动态的设置中对分页结果更有效。

答案 2 :(得分:4)

好像你可以重载save类的Vote并让它更新相应的Link对象。像这样的东西应该运作良好:

from datetime import datetime, timedelta

class Link(models.Model):
 category = models.ForeignKey(Category)
 user = models.ForeignKey(User)
 created = models.DateTimeField(auto_now_add = True)
 modified = models.DateTimeField(auto_now = True)
 fame = models.PositiveIntegerField(default = 1)
 title = models.CharField(max_length = 256)
 url = models.URLField(max_length = 2048)

 #a field to keep the most recently calculated popularity
 popularity = models.FloatField(default = None)

 def CalculatePopularity(self):
  """
  Add a shorcut to make life easier ... this is used by the overloaded save() method and 
  can be used in a management function to do a mass-update periodically
  """
  ts = datetime.now()-self.created
  th = ts.seconds/60/60
  self.popularity = (self.user_set.count()-1)/((th+2)**1.5)

 def save(self, *args, **kwargs):
  """
  Modify the save function to calculate the popularity
  """
  self.CalculatePopularity()
  super(Link, self).save(*args, **kwargs)


 def __unicode__(self):
     return self.title

class Vote(models.Model):
 link = models.ForeignKey(Link)
 user = models.ForeignKey(User)
 created = models.DateTimeField(auto_now_add = True)
 modified = models.DateTimeField(auto_now = True)
 karma_delta = models.SmallIntegerField()

 def save(self, *args, **kwargs):
  """
  Modify the save function to calculate the popularity of the Link object
  """
  self.link.CalculatePopularity()
  super(Vote, self).save(*args, **kwargs)

 def __unicode__(self):
     return str(self.karma_delta)

这样每次调用link_o.save()或vote_o.save()时,都会重新计算流行度。您必须要小心,因为当您致电Link.objects.all().update('updating something')时,它不会调用我们重载的save()函数。因此,当我使用这种东西时,我创建了一个管理命令,它可以更新所有对象,因此它们不会过时。像这样的东西会很有效:

from itertools import imap
imap(lambda x:x.CalculatePopularity(), Link.objects.all().select_related().iterator())

这样它一次只能将一个Link对象加载到内存中...所以如果你有一个巨大的数据库,它不会导致内存错误。

现在要做你的排名,你所要做的就是:

Link.objects.all().order_by('-popularity')

由于你们所有的链接项目已经计算了受欢迎程度,因此速度非常快。

答案 3 :(得分:1)

这是我的问题的最终答案,虽然已经很晚几个月了,而不是我的想法。希望它对某些人有用。

def hot(request):
    links = Link.objects.select_related().annotate(votes=Count('vote')).order_by('-created')[:150]
    for link in links:
        delta_in_hours = (int(datetime.now().strftime("%s")) - int(link.created.strftime("%s"))) / 3600
        link.popularity = ((link.votes - 1) / (delta_in_hours + 2)**1.5)

    links = sorted(links, key=lambda x: x.popularity, reverse=True)

    links = paginate(request, links, 5)

    return direct_to_template(
        request,
        template = 'links/link_list.html',
        extra_context = {
            'links': links
        })

这里发生了什么是我提取最新的150份提交(每页30个链接5页),如果你需要更多,你可以通过改变我的切片[:150]来获取。这样我就不必遍历我的查询集,这可能最终会变得非常大,真的150个链接应该足以让任何人拖延。

然后我计算从现在到创建链接的时间差,并将其变成几小时(不像我想象的那么容易)

将算法应用于不存在的字段(我喜欢这种方法,因为我不必将值存储在我的数据库中,并且不依赖于周围的链接。

for循环之后的那一行是我还有一点麻烦的地方。我不能order_by('popularity'),因为它不是我的数据库中的真实字段,并且是动态计算的,所以我必须将我的查询集转换为对象列表并从那里排序流行度。

下一行只是我的paginator快捷方式,幸好分页不需要查询集,不像一些通用视图(与你对话object_list)。

将所有东西吐出一个漂亮的direct_to_template通用视图,并以我的快乐方式。