Python 按日期统计daterange中的对象数

Python 按日期统计daterange中的对象数,python,django,performance,django-orm,Python,Django,Performance,Django Orm,在Django项目中,我定义了以下简化模型: class People(models.Model): name = models.CharField(max_length=96) class Event(models.Model): name = models.CharField(verbose_name='Nom', max_length=96) date_start = models.DateField() date_end = models.DateF

在Django项目中,我定义了以下简化模型:

class People(models.Model):
    name = models.CharField(max_length=96)

class Event(models.Model):

    name = models.CharField(verbose_name='Nom', max_length=96)

    date_start = models.DateField()
    date_end = models.DateField()

    participants = models.ManyToManyField(to='People', through='Participation')

class Participation(models.Model):
    """Represent the participation of 1 people to 1 event, with information about arrival date and departure date"""

    people = models.ForeignKey(to=People, on_delete=models.CASCADE)
    event = models.ForeignKey(to=Event, on_delete=models.CASCADE)

    arrival_d = models.DateField(blank=True, null=True)
    departure_d = models.DateField(blank=True, null=True)
现在,我需要生成一个参与图:对于每个活动日,我需要相应的参与总数。 目前,我使用的代码很糟糕:

def daterange(start, end, include_last_day=False):
    """Return a generator for each date between start and end"""
    days = int((end - start).days)
    if include_last_day:
        days += 1
    for n in range(days):
        yield start + timedelta(n)

class ParticipationGraph(DetailView):

    template_name = 'events/participation_graph.html'
    model = Event

    def get_context_data(self, **kwargs):

        labels = []
        data = []

        for d in daterange(self.object.date_start, self.object.date_end):
            labels.append(formats.date_format(d, 'd/m/Y'))
            total_participation = self.object.participation_set
                .filter(arrival_d__lte=d, departure_d__gte=d).count()
            data.append(total_participation)

        kwargs.update({
            'labels': labels,
            'data': data,
        })
        return super(ParticipationGraph, self).get_context_data(**kwargs)
显然,我在
Event.date\u start
Event.date\u end
之间每天都运行一个新的SQL查询是否有一种方法可以通过减少SQL查询的数量(理想情况下,只有一个)获得相同的结果?


我尝试了Django orm中的许多聚合工具(values()、distinct()等),但我总是遇到相同的问题:我没有一个带有简单日期值的字段,我只有开始和结束日期(在事件中)以及出发和到达日期(在参与中),因此,我找不到按日期对结果进行分组的方法。

我同意当前方法的成本很高,因为每天都要在数据库中查询您之前检索到的参与者。相反,我将通过对DB进行一次性查询来获取参与者,然后使用该数据填充结果数据结构

我对您的解决方案所做的一个结构性改变是,不再跟踪两个列表,其中每个索引对应一天和参与人数,而是在一个字典中聚合数据,将当天映射到参与人数。如果我们以这种方式聚合结果,我们总是可以在需要时将其转换为最后的两个列表

以下是我的一般(伪代码)方法:

def formatDate(d):
    return formats.date_format(d, 'd/m/Y')

def get_context_data(self, **kwargs):

    # initialize the results with dates in question
    result = {}
    for d in daterange(self.object.date_start, self.object.date_end):
        result[formatDate(d)] = 0

    # for each participant, add 1 to each date that they are there
    for participant in self.object.participation_set:
        for d in daterange(participant.arrival_d, participant.departure_d):
            result[formatDate(d)] += 1

    # if needed, convert result to appropriate two-list format here

    kwargs.update({
        'participation_amounts': result
    })
    return super(ParticipationGraph, self).get_context_data(**kwargs)
就性能而言,两种方法执行相同数量的操作。在你的方法中,每天d,你过滤每个参与者p。因此,操作的数量是O(dp)。在我的方法中,对于每一个参与者,我每天都要经历他们参加的过程(每天的演员阵容都很糟糕,d)。因此,它也是O(dp)


选择我的方法的原因是你指出的。它只会点击数据库一次以检索参与者列表。因此,它对网络延迟的依赖性较小。它确实牺牲了通过python代码执行SQL查询所获得的一些性能优势。然而,python代码并不太复杂,对于有几十万人的事件来说应该相当容易处理。

我几天前看到了这个问题,并对它进行了一次投票,因为它写得非常好,问题也非常有趣。最后,我找到了一些时间致力于它的解决方案

Django是名为模型模板视图的模型视图控制器的变体。因此,我的方法将遵循“胖模型和瘦控制器”的范例(或翻译为符合Django的“胖模型和瘦视图”)

以下是我将如何重写模型:

import pandas

from django.db import models
from django.utils.functional import cached_property


class Person(models.Model):
    name = models.CharField(max_length=96)


class Event(models.Model):
    name = models.CharField(verbose_name='Nom', max_length=96)
    date_start = models.DateField()
    date_end = models.DateField()
    participants = models.ManyToManyField(to='Person', through='Participation')

    @cached_property
    def days(self):
        days = pandas.date_range(self.date_start, self.date_end).tolist()
        return [day.date() for day in days]

    @cached_property
    def number_of_participants_per_day(self):
        number_of_participants = []
        participations = self.participation_set.all()
        for day in self.days:
            count = len([par for par in participations if day in par.days])
            number_of_participants.append((day, count))
        return number_of_participants


class Participation(models.Model):
    people = models.ForeignKey(to=Person, on_delete=models.CASCADE)
    event = models.ForeignKey(to=Event, on_delete=models.CASCADE)
    arrival_d = models.DateField(blank=True, null=True)
    departure_d = models.DateField(blank=True, null=True)

    @cached_property
    def days(self):
        days = pandas.date_range(self.arrival_d, self.departure_d).tolist()
        return [day.date() for day in days]
所有计算都放置在模型中。依赖于数据库中存储的数据的信息可用作

让我们看一个
事件的示例:

djangocon = Event.objects.create(
    name='DjangoCon Europe 2018',
    date_start=date(2018,5,23),
    date_end=date(2018,5,28)
)
djangocon.days
>>> [datetime.date(2018, 5, 23),
     datetime.date(2018, 5, 24),
     datetime.date(2018, 5, 25),
     datetime.date(2018, 5, 26),
     datetime.date(2018, 5, 27),
     datetime.date(2018, 5, 28)]
我使用了
pandas
来生成日期范围,这对于您的应用程序来说可能有点过分,但是它有很好的语法,并且非常适合用于演示目的。您可以用自己的方式生成日期范围。
要得到这个结果,只有一个查询。
days
与任何其他字段一样可用。
与我在
参与
中所做的相同,以下是一些示例:

antwane = Person.objects.create(name='Antwane')
rohan = Person.objects.create(name='Rohan Varma')
cezar = Person.objects.create(name='cezar')
他们都想在2018年访问DjangoCon Europe,但并非所有人都全天出席:

p1 = Participation.objects.create(
    people=antwane,
    event=djangocon,
    arrival_d=date(2018,5,23),
    departure_d=date(2018,5,28)
)
p2 = Participation.objects.create(
    people=rohan,
    event=djangocon,
    arrival_d=date(2018,5,23),
    departure_d=date(2018,5,26)
)
p3 = Participation.objects.create(
    people=cezar,
    event=djangocon,
    arrival_d=date(2018,5,25),
    departure_d=date(2018,5,28)
)
现在我们想看看活动每天有多少参与者。我们还跟踪SQL查询的数量

from django.db import connection
djangocon = Event.objects.get(pk=1)
djangocon.number_of_participants_per_day
>>> [(datetime.date(2018, 5, 23), 2),
     (datetime.date(2018, 5, 24), 2),
     (datetime.date(2018, 5, 25), 3),
     (datetime.date(2018, 5, 26), 3),
     (datetime.date(2018, 5, 27), 2),
     (datetime.date(2018, 5, 28), 2)]

connection.queries
>>>[{'time': '0.000', 'sql': 'SELECT "participants_event"."id", "participants_event"."name", "participants_event"."date_start", "participants_event"."date_end" FROM "participants_event" WHERE "participants_event"."id" = 1'},
    {'time': '0.000', 'sql': 'SELECT "participants_participation"."id", "participants_participation"."people_id", "participants_participation"."event_id", "participants_participation"."arrival_d", "participants_participation"."departure_d" FROM "participants_participation" WHERE "participants_participation"."event_id" = 1'}]
有两个问题。第一个获取对象
事件
,第二个获取事件每天的参与者数量

现在,您可以随心所欲地在视图中使用它。由于缓存属性,您无需重复数据库查询即可获得结果

您可以遵循相同的原则,也可以添加属性以列出活动每天的所有参与者。它可能看起来像:

class Event(models.Model):
    # ... snip ...
    @cached_property
    def participants_per_day(self):
        participants  = []
        participations = self.participation_set.all().select_related('people')
        for day in self.days:
            people = [par.people for par in participations if day in par.days]
            participants.append((day, people))
        return participants

    # refactor the number of participants per day
    @cached_property
    def number_of_participants_per_day(self):
        return [(day, len(people)) for day, people in self.participants_per_day]

我希望您喜欢这个解决方案。

这是一个非常优雅和聪明的解决方案。我做了测试,它完全符合我的需要。在执行了一些优化(与此处未提及的某些特定性相关)之后,您的解决方案执行所需的时间大致相同,但减少了大量查询(特别是对于长事件)。我太专注于如何仅使用SQL来实现这一点,但添加一些Python逻辑也非常有效。谢谢也许你也应该用
python
来标记你的问题。这将引起更多的关注。另外,
django-orm
似乎比
orm
更合适。谢谢,我已经按照你的建议做了是的,我也喜欢你的解决方案。我在我的模型中使用了很多缓存属性,并且将活动的天数和每次参与的天数列出来是有意义的,因为这是我以后可能会重用的信息。我实现并检查了呈现页面所需的时间和查询数量。这相当于Rohan的解决方案。谢谢@Antwane在性能方面,该解决方案可以与Rohan的解决方案相媲美,但无法击败它。你不能低于1个数据库命中率,这个限制不能被打破。但是,我强烈建议您将业务逻辑和所有数据库操作放在模型层中。请检查编辑以查看如何扩展解决方案的示例。