Django: Get average rating per user

Question:

Given the following, how can I make a query which returns a list of the average rating per user in friendlist?

models.py

class Beer(models.Model):
    id = models.BigIntegerField(primary_key=True)
    name = models.CharField(max_length=150)
    ...

class Checkin(models.Model):
    id = models.IntegerField(primary_key=True)
    rating = models.FloatField(blank=True, null=True)
    user = models.ForeignKey(User, on_delete=CASCADE)
    ...

class FriendList(models.Model):
    user = models.OneToOneField(User, on_delete=CASCADE, primary_key=True)
    friend = models.ManyToManyField(User, related_name="friends")

database (postgresql)

user beer rating
1 1 4.2
1 1 3.5
1 1 4.0
2 1 4.1
2 1 3.7

My current query to get all friends checkins:

Checkin.objects.filter(beer=1, user__in=friends.friend.all())

Which gives me something like:

[{user: 1, rating: 4.2}, {user: 1, rating: 3.5},...,{user: 2, rating: 4.1}...]

What I want is:

[{user: 1, avg_rating: 4.1}, {user: 2, avg_rating: 3.8}]
Asked By: Haavardnk

||

Answers:

It makes more sense to .annotate(…) [Django-doc] the User objects, so:

from django.db.models import Avg

friends.friend.filter(
    checkin__beer_id=1
).annotate(
    rating=Avg('checkin__rating')
)

Where checkin__ is the related_query_name=… [Django-doc] for the user from Checkin to the User model. If you did not specify a related_query_name=…, then it will use the value for the related_name=… [Django-doc], and if that one is not specified either, it will use the name of the source model in lowercase, so checkin.

The User objects that arise from this queryset will have an extra attribute .rating that contains the average rating over the Checkins for that beer_id.

You can determine the average of these averages with an .aggregate(…) call [Django-doc]:

from django.db.models import Avg

friends.friend.filter(
    checkin__beer_id=1
).annotate(
    rating=Avg('checkin__rating')
).aggregate(
    all_users_avg_rating=Avg('rating'),
    number_of_users=Count('pk')
)

This will return a dictionary with two elements: all_users_avg_rating will map to the average of these averages, and number_of_users will return the number of distinct users.


Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.

Answered By: Willem Van Onsem