Django ORM:获取每个类别的每月平均价格

Django ORM: get the monthly average of price for each category

想象一下这个简单的模型:

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()

我正在尝试获取当年每个 category 的月平均值 price。我的一般想法是做类似的事情:

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"))))

我主要是想:

  1. 截断 created_at
  2. 的月份
  3. categorymonth
  4. 分组
  5. 求和 prices
  6. 为每个 category
  7. 聚合 prices

如果我愿意,它工作得很好:

for category in categories:
   sub.filter(category=category).aggregate(avg=Avg("total"))

模型管理器中的一个函数可能是一个很好的解决方案。在您的 models.py 中,在您的 Expense class:

之前添加一个名为 ExpenseManagermodels.Manager class
class ExpenseManager(models.Manager):
  def summarise_categories(self):
    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")/Count("created_at__month", distinct=True))
   )

然后在你 Expense class 做:

class Expense(models.Model):
  ...
  
  objects = ExpenseManager()

要获得生成的查询集聚合,您只需在视图中的模型对象上调用模型管理器方法即可:

summarised_categories = Expense.objects.summarise_categories()

这种方法将所有工作推送到数据库,返回类别字典及其 year-to-date 的平均每月价格。

您的查询可能比您想象的要简单。您当前的解决方案尝试是:

  1. 截断 created_at 以获得月份
  2. categorymonth
  3. 分组
  4. 总和prices
  5. 取每个类别总和的平均值

这个问题是采用聚合的聚合。让我们反过来考虑您的问题(我们将在这里做一些数学运算)。你想要一个类别的月平均价格,如果我们只考虑一个类别并且每月价格是一个数组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 意味着我们假设我们有全年的数据,这可能不适用于当前年份,因此我们应该除以适当的月数。我们可能还想过滤到上个月,以防我们没有明显进入当前月份。