2017-01-27 2 views
2

Я хотел бы суммировать все продолжительности события в день. Это моя модель:Агрегатная сгруппированная аннотация

class Event(models.Model): 
    start = models.DateTimeField() 
    end = models.DateTimeField() 

Образец данных:

import datetime 
from random import randint 

for i in range(0, 1000): 
    start = datetime.datetime(
     year=2016, 
     month=1, 
     day=randint(1, 10), 
     hour=randint(0, 23), 
     minute=randint(0, 59), 
     second=randint(0, 59) 
    ) 
    end = start + datetime.timedelta(seconds=randint(30, 1000)) 
    Event.objects.create(start=start, end=end) 

я могу рассчитывать событие в день, как так: (я знаю extra это плохо, но я использую 1.9 в данный момент . Когда я обновляю я перейду к использованию TruncDate)

Event.objects.extra({'date': 'date(start)'}).order_by('date').values('date').annotate(count=Count('id')) 

[{'count': 131, 'date': datetime.date(2016, 1, 1)}, 
{'count': 95, 'date': datetime.date(2016, 1, 2)}, 
{'count': 99, 'date': datetime.date(2016, 1, 3)}, 
{'count': 85, 'date': datetime.date(2016, 1, 4)}, 
{'count': 87, 'date': datetime.date(2016, 1, 5)}, 
{'count': 94, 'date': datetime.date(2016, 1, 6)}, 
{'count': 97, 'date': datetime.date(2016, 1, 7)}, 
{'count': 111, 'date': datetime.date(2016, 1, 8)}, 
{'count': 97, 'date': datetime.date(2016, 1, 9)}, 
{'count': 104, 'date': datetime.date(2016, 1, 10)}] 

можно аннотировать добавить продолжительность:

In [3]: Event.objects.annotate(duration=F('end') - F('start')).first().duration 
Out[3]: datetime.timedelta(0, 470) 

Но я не могу понять, как суммировать эту аннотацию так же, как я могу подсчитывать события. Я пробовал следующее, но я получил KeyError по длительности.

Event.objects.annotate(duration=F('end') - F('start')).extra({'date': 'date(start)'}).order_by('date').values('date').annotate(total_duration=Sum('duration')) 

А если добавить duration к статье values тогда он больше не групп по дате.

Возможно ли это в одном запросе и без добавления поля длительности в модель?

+0

Когда вы выполняете 'values ​​('date')' на этом наборе запросов, вы удаляете поле 'duration', поэтому сумма не суммируется. Что произойдет, если вы добавите 'duration' в вызов 'values ​​()' и затем 'order_by ('date')' после этого? Или, нужно ли вообще использовать 'values ​​()'? – ChidG

+1

Привет @ChidG! (как Ze'ev?) 'values' используется для group_by - я думаю, что это необходимо. Если я добавляю 'duration' к вызову' values', он пытается группировать события вместе как «date», так и «duration», а не просто «date». –

ответ

2

Я собирался написать ответ, что Django ORM не поддерживает это. И да, тогда я потратил еще час на эту проблему (в дополнение к 1,5 часа, которые были потрачены, прежде чем начать писать этот ответ), но, как оказалось, Django действительно поддерживает это. И без взлома. Хорошие новости!

import datetime as dt 

from django.db import models 
from django.db.models import F, Sum, When, Case 
from django.db.models.functions import TruncDate 

from app.models import Event 

a = Event.objects.annotate(date=TruncDate('start')).values('date').annotate(
    day_duration=Sum(Case(
     When(date=TruncDate(F('start')), then=F('end') - F('start')), 
     default=dt.timedelta(), output_field=models.DurationField() 
    )) 
) 

И некоторые предварительные испытания (надеюсь) доказывают, что этот материал фактически делает то, что вы просили.

In [71]: a = Event.objects.annotate(date=TruncDate('start')).values('date').annotate(day_duration=Sum(Case(
    ...:   When(date=TruncDate(F('start')), then=F('end') - F('start')), 
    ...:   default=dt.timedelta(), output_field=models.DurationField() 
    ...: )) 
    ...:) 

In [72]: for e in a: 
    ...:  print(e) 
    ...:  
{'day_duration': datetime.timedelta(0, 41681), 'date': datetime.date(2016, 1, 10)} 
{'day_duration': datetime.timedelta(0, 46881), 'date': datetime.date(2016, 1, 3)} 
{'day_duration': datetime.timedelta(0, 48650), 'date': datetime.date(2016, 1, 1)} 
{'day_duration': datetime.timedelta(0, 52689), 'date': datetime.date(2016, 1, 8)} 
{'day_duration': datetime.timedelta(0, 45788), 'date': datetime.date(2016, 1, 5)} 
{'day_duration': datetime.timedelta(0, 49418), 'date': datetime.date(2016, 1, 7)} 
{'day_duration': datetime.timedelta(0, 45984), 'date': datetime.date(2016, 1, 9)} 
{'day_duration': datetime.timedelta(0, 51841), 'date': datetime.date(2016, 1, 2)} 
{'day_duration': datetime.timedelta(0, 63770), 'date': datetime.date(2016, 1, 4)} 
{'day_duration': datetime.timedelta(0, 57205), 'date': datetime.date(2016, 1, 6)} 

In [73]: q = dt.timedelta() 

In [74]: o = Event.objects.filter(start__date=dt.date(2016, 1, 7)) 

In [75]: p = Event.objects.filter(start__date=dt.date(2016, 1, 10)) 

In [76]: for e in o: 
    ...:  q += (e.end - e.start) 

In [77]: q 
Out[77]: datetime.timedelta(0, 49418) # Matches 2016.1.7, yay! 

In [78]: q = dt.timedelta() 

In [79]: for e in p: 
    ...:  q += (e.end - e.start) 

In [80]: q 
Out[80]: datetime.timedelta(0, 41681) # Matches 2016.1.10, yay! 

NB! Это работает с версии 1.9, я не думаю, что вы можете сделать это с более ранними версиями, потому что отсутствует функция TruncDate. А до 1.8 у вас, конечно, нет Case и When штучек.

+0

Это очень здорово, молодцы! (Также замечательно видеть другого джангонаута с фоном мехатроники) –

Смежные вопросы