【问题标题】:Implementing a popularity algorithm in Django在 Django 中实现流行度算法
【发布时间】:2010-12-30 05:40:47
【问题描述】:

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

Y Combinator 的黑客新闻: 人气 = (p - 1) / (t + 2)^1.5` 选票除以年龄因素。 哪里` p :来自用户的投票(积分)。 t :自提交以来的时间,以小时为单位。 p 减 1 以否定提交者的投票。 年龄因子是(以小时为单位的提交时间加 2)的 1.5 次方。因子是(以小时为单位的提交时间加 2)的 1.5 次方。

我在 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})

现在,从我之前的问题来看,我正在尝试使用排序函数来实现算法。该问题的答案似乎认为我应该将算法放在选择中然后排序。我将对这些结果进行分页,因此我认为如果不抓取所有内容,我就无法在 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 很简单:

{% 块内容 %} {% for link in links %} 业力提升 {{ 链接.karma_total }} 业力下降 {{ 链接.title }} 由 {{ link.user }} 在 {{ link.created }} 发布到 {{ link.category }} {% 空的 %} 没有链接 {% endfor %} {% endblock 内容 %}

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

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

定义索引(请求): popular_links = Link.objects.select_related().extra( 选择 = { '受欢迎程度': '(SUM(links_vote.karma_delta) - 1) / POW(2, 1.5)', }, 表 = ['links_link', 'links_vote'], order_by = ['-人气'], ) 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 不会在我的 group by 但我不知道如何更改我的 group by,django 通常会这样做。

【问题讨论】:

  • 嗯,吃光了 HTML。啊,好吧,我最不用担心了。

标签: python django algorithm postgresql


【解决方案1】:

在 Hacker News 上,只有 210 个最新故事和 210 个最受欢迎的故事被分页(7 页价值 * 30 个故事)。我的猜测是限制的原因(至少部分)是这个问题。

为什么不放弃最流行的故事的所有花哨的 SQL,而只保留一个运行列表呢?一旦你建立了前 210 个故事的列表,你只需要担心当有新的投票出现时重新排序,因为随着时间的推移会保持相对顺序。当确实有新的投票时,您只需要担心重新排序收到投票的故事。

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

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

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

【讨论】:

  • 这是一个非常棒的主意。我从没想过这个网站会变得非常大,因此必须对这些数字进行一些重大调整,但我喜欢它的发展方向。
  • 晚了几个月,但我又回来了。您不介意进一步详细说明吗?您将如何跟踪您的跑步清单?另一个数据库表?它会在每个 Http 请求上即时计算吗?不要担心投反对票,我得出的结论是,在大多数情况下投反对票是个坏主意。 buildingreputation.com 有一些很好的读物。
【解决方案2】:
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 在高度动态的设置中更适合分页结果。

【讨论】:

  • 我真的认为这就是我想要的,尽管由于某种原因我不断得到同样该死的错误:渲染时捕获异常:列“karma_total”不存在第 1 行:SELECT ( (karma_total - 1) / POW(2, 1.5)) 作为“受欢迎程度”,(S...这很奇怪,因为我认为 karma_total 已经在其上方定义了一行!
  • 如果您在最终结果中不需要它,只需替换它即可:'popularity': '(SUM(vote.karma_delta) - 1) / POW(2, 1.5)'
  • 伙计,这是我希望最有效的特定答案。其他答案很好,但我希望这个能工作。渲染时遇到异常:缺少表“投票”第 1 行的 FROM 子句条目:SELECT ((vote.karma_total - 1) / POW(2, 1.5)) AS “popularity ... 是我的最新错误。这不会与我使用 1.1.1 而不是开发版本有什么关系吗?
  • 你的投票表没​​有被select_related添加到查询中;如果您的投票表称为vote,您可以通过修改 extra 并添加 tables = ['vote'] 来手动添加它
  • 查看我的编辑 3 代码。我越来越近了,我可以品尝到它!我收到一个错误,指的是我的 group by 中没有适当的值。考虑到 django 通常会处理这些问题,不知道如何添加它们。
【解决方案3】:

似乎您可以重载Vote 类的save 并让它更新相应的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')

这将是超快的,因为你们所有的链接项目都已经计算了受欢迎程度。

【讨论】:

  • 这是否意味着每次对链接进行投票时,都会更新每个链接行?
  • 不......它只会更新被投票的链接......所以它不会是处理器密集型
【解决方案4】:

这是我问题的最终答案,虽然晚了好几个月,但并不完全是我的想法。希望它对某些人有用。

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 份提交(5 页,每页 30 个链接),如果您需要更明显的内容,您可以通过更改我的切片 [:150] 来获取它们。这样我就不必遍历我的查询集,它最终可能会变得非常大,而且实际上 150 个链接对于任何人来说都足够拖延了。

然后我计算现在和创建链接之间的时间差,并将其转换为小时(不像我想象的那么容易)

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

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

下一行只是我的分页器快捷方式,谢天谢地,分页不需要查询集,不像一些通用视图(与您交谈 object_list)。

将所有内容都吐到一个漂亮的 direct_to_template 通用视图中,然后继续我的快乐之路。

【讨论】:

  • 作为旁注,有一种更简单的方法可以使用 datetime.timedelta 计算 delta_in_hours:(datetime.now() - link.created).total_seconds() / 3600
猜你喜欢
  • 2011-01-30
  • 2014-02-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-03
  • 2012-06-23
  • 1970-01-01
相关资源
最近更新 更多