Быстрое вычисление скользящего среднего с Django ORM

Мы запускаем Postgres 9.6.5 и Django 2.0. У нас есть Model с полями created_at а также value, Нам нужно рассчитать 90-дневную скользящую среднюю для определенного date_range, Вот как мы это делаем:

output = []

for i in range(len(date_range)):
    output.append(
        Model.objects.filter(
            created_at__date__range=(date_range[i]-timezone.timedelta(days=90), date_range[i]),
        ).aggregate(Avg('value'))['value__avg'].days
    )

Это использует Avg агрегатная функция, поэтому она достаточно быстрая, однако нам нужен один запрос на каждую дату в date_range, Для более длинных диапазонов это означает много запросов.

Postgres может сделать это в одном запросе. Мой вопрос - можем ли мы как-то сделать это в одном запросе, используя Django ORM?

(Я знаю, что могу выполнять сырой SQL с Django ORM, но я хотел избежать этого, если это возможно, поэтому я и спрашиваю.)

3 ответа

Предполагая, что у вас есть одна запись на дату, вы можете использовать выражения нового окна Django 2.0 для вычисления 90-периодной скользящей средней в одном запросе:

from django.db.models import Avg, F, RowRange, Window

items = Model.objects.annotate(
    avg=Window(
        expression=Avg('value'), 
        order_by=F('created_at').asc(), 
        frame=RowRange(start=-90,end=0)
    )
)

См. Также ValueRange, если вы хотите вместо этого формировать кадры по определенным значениям поля, что может пригодиться, например, если у вас есть несколько строк для каждого дня.

Еще одна попытка. Это более эффективно, поскольку он использует только один запрос, но выбирает все необходимые экземпляры модели из БД для выполнения логики в python вместо уровня БД. Все еще не оптимально, но, надеюсь, на этот раз все будет правильно;) Вы должны сравнить, действительно ли это дает улучшение производительности в вашем случае.

import numpy as np
instances =  Model.objects.filter(
        created_at__gte=min(date_range)-timezone.timedelta(days=90),
        created_at__lte=max(date_range)
    ).values('created_at', 'value')

instances = list(instances)  # evaluate QuerySet and hit DB only once

output = []
for i in range(len(date_range)):    
    output.append(
        np.mean(np.array([inst.value for inst in instances if \
            inst.created_at >= date_range[i]-timezone.timedelta(days=90) and \
            inst.created_at <  date_range[i]
        ]))
    )

Вместо агрегирования вы можете использовать аннотацию. Учтите это, когда я начинаю тестирование, я не совсем уверен в приведенном ниже коде. Смотрите также документы о F() объекты

    Model.objects.annotate(
        value_avg=Avg(
            'value',
            filter=Q(
                created_at__date__range=(
                    F('created_at__date')-timezone.timedelta(days=90),
                    F('created_at__date')
                )
            )
        )
    )

your_date_field зависит что ты

Другие вопросы по тегам