Django Query: Return average monthly spend per user

Question:

Given a Django model which stores user transactions, how can I create a query that returns the average monthly spend for each user?

My current solution queries all the transactions , iterates the result and calculates the averages for each user using a dictionary. I am aware that there is a more efficient way to query this using aggregate/annotate but unsure how to write it.

I am more concerned about readability than speed since the number of transactions in the db is relatively small and will never change.

models.py

Class Transactions(models.Model):
  id = models.AutoField(primary_key=True)
  user = models.ForeignKey('User', on_delete=models.CASCADE)
  amount = models.DecimalField(max_digits=100, decimal_places=2)
  date = models.DateField()
Asked By: Tamdim

||

Answers:

Taking a late night stab at this (untested). The code below extracts out the year and month from the date, then clear the order with order_by() (may not be necessary in all cases), then group by the ‘user’, ‘year’, ‘month’ and calculate the average, storing in a column named ‘average’ by using the Avg function.

Documentation has some other good examples.

from django.db.models.functions import ExtractMonth, ExtractYear
from django.db.models import Avg
...

avg_by_user_by_month = Transactions.objects
                                   .annotate(month=ExtractMonth('date'),
                                             year=ExtractYear('date'),) 
                                   .order_by()
                                   .values('user', 'year', 'month')
                                   .annotate(average=Avg('amount'))
                                   .values('user', 'year', 'month', 'average')
        

EDIT
Alternatively, this may also work:

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

avg_by_user_by_month = Transactions.objects
                                   .annotate(month=F('date__month'),
                                             year=F('date__year'),) 
                                   .order_by()
                                   .values('user', 'year', 'month')
                                   .annotate(average=Avg('amount'))
                                   .values('user', 'year', 'month', 'average')
        
Answered By: AMG