Django ORM: получить среднемесячное значение цены для каждой категории
Представьте себе эту простую модель:
class Expense(models.Model):
price = models.DecimalField(decimal_places=2, max_digits=6)
description = models.CharField(max_length=300)
category = models.CharField(choices=ExpenseCategory.choices, max_length=20)
created_at = models.DateField()
Я пытаюсь получить среднемесячное значение price
для каждого category
в текущем году. Моя общая идея заключалась в том, чтобы сделать что-то вроде:
sub = (
Expense.objects.filter(created_at__year=date.today().year)
.annotate(month=TruncMonth("created_at"))
.values("month", "category")
.annotate(total=Sum("price"))
.order_by("month")
)
qs = Expense.objects.values("category").annotate(avg=Avg(Subquery(sub.values("total"))))
Я в основном пытаюсь:
- Усечение месяцев
created_at
- Сгруппируйте по
category
иmonth
- Сумма
prices
- Суммируйте
prices
для каждогоcategory
Все работает нормально, если я делаю так:
for category in categories:
sub.filter(category=category).aggregate(avg=Avg("total"))
Хорошим решением может стать функция внутри менеджера моделей. В вашем models.py
добавьте класс models.Manager перед вашим ExpenseManager
классом: Expense
class ExpenseManager(models.Manager):
def summarise_categories():
return (
super(ExpenseManager, self)
.get_queryset()
.filter(created_at__year=date.today().year)
.annotate(month=TruncMonth("created_at"))
.values("month", "category")
.annotate(total=Sum("price"))
.values("category")
.aggregate(ave = Avg("total"))
)
then in you Expense
class do:
class Expense(models.Model):
...
objects = ExpenseManager()
Для получения результирующей агрегации наборов запросов вы можете просто вызвать метод менеджера модели на объектах модели внутри представления:
summarised_categories = Expense.objects.summarise_categories()
При таком подходе вся работа перекладывается на базу данных, которая возвращает словарь категорий и их среднемесячные цены за прошедший год.
Ваш запрос может быть более простым, чем вы думаете. Ваша текущая попытка решения такова:
- Усечение
created_at
для получения месяца - Сгруппируйте по
category
иmonth
- Сумма
prices
- Возьмите среднее значение суммы для каждой категории
Проблема заключается в том, чтобы взять совокупность из совокупности. Давайте представим вашу проблему в обратном виде (здесь мы немного займемся математикой). Вам нужно среднее значение месячной цены категории, если мы рассматриваем только одну категорию и месячные цены как массив M[12]
, тогда мы можем выразить это как:
(M[0] + M[1] + ... + M[11]) / 12
Каждое из значений в M можно рассматривать как суммирование prices
, где месяц совпадает. Если рассматривать P[12][] как двумерный массив, содержащий цены для каждого месяца, то вышеприведенную формулу можно переписать следующим образом:
(Sum(P[0]) + Sum(P[1] + ... + Sum(P[12])) / 12
Подумав об этом дальше, можно сказать, что это просто сумма всех цен за год, деленная на 12! Это означает, что ваш запрос можно записать так:
from django.db.models import ExpressionWrapper, FloatField, Sum, Value
qs = Expense.objects.filter(
created_at__year=date.today().year
).values("category").annotate(
avg=ExpressionWrapper(
Sum("price") / Value(12), output_field=FloatField()
)
)
Примечание: Деление на 12 означает, что мы предполагаем, что у нас есть данные за весь год, что, вероятно, не верно для текущего года, поэтому вместо этого мы должны делить на соответствующее количество месяцев. Мы также можем захотеть отфильтровать данные до предыдущего месяца, если текущий месяц не является для нас значительным.