Django: Get average rating per user - python

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}]

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.

Related

Count & Sum of Order Values for each customer (through iteration) in Django

I have Customer & Order models as below:
class Customer(models.Model):
name = models.CharField(max_length = 100)
city = models.CharField(max_length = 100)
class Order(models.Model):
value = models.FloatField()
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
Now I would like to generate a table of (distinct) customers along with a count of number of orders placed by each of them and the sum of values of those orders. I tried this in the views.py:
def customers(request):
customer_orders = Order.objects.distinct().annotate(Sum('value'))
Then in my html template, I tried the following:
<ul>
{% for customer in customer_orders %}
<li>{{customer.customer}} - {{customer.value__sum}}<li>
{% endfor %}
</ul>
After all this, instead of getting unique customers (and respective order records), I'm getting a list of all orders and customers are getting repeated (as shown below). Not sure what I'm missing here.
Bosco-Ward - 16,700.0
Ernser PLC - 51,200.0
Murphy Ltd - 21,400.0
Kohler-Veum - 29,200.0
Schmidt-Legros - 96,800.0
Brown-Weissnat - 8,200.0
Bosco-Ward - 36,400.0
Ernser PLC - 66,600.0
Murphy Ltd - 84,200.0
Also wanted to know if there's a possibility to generate a table of city names with order count and total value of orders received from that city (note that my order model doesn't have city field).
Since you want a queryset of Customer instances make your query on the Customer model itself instead of on Order, next I believe you will not need to use distinct here since the customer instances should be considered unique. Hence, you can make a query like:
from django.db.models import Count, Sum
customers = Customer.objects.annotate(order_count=Count('order'), order_value_sum=Sum('order__value'))
for customer in customers:
print(customer.name, customer.order_count, customer.order_value_sum)
Kindly note im typing the solution from my phone without testing it but this is what i think:
Give a related name to customer in Order model:
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name=“orders”)
Get orders count:
Customer.objects.get(id=1).orders.count()
Or access this attribute in template something like:
{{ customer.orders.count }}
Get orders count values by city
From django.db.models import Count, Sum
Customer.objects.values(“city”).annotate(order_count=Count(“orders”)).annotate(totals=Sum(“value”))
Add realted_name to Order customer field:
class Customer(models.Model):
name = models.CharField(max_length = 100)
city = models.CharField(max_length = 100)
class Order(models.Model):
value = models.FloatField()
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='orders')
query:
Customer.objects.values(“city”).annotate(order_count=Count(“orders”)).annotate(totals=Sum(“orders__value”))

Django aggregate Avg vs for loop with prefetch_related()

I have a Review model that is in one to one relationship with Rating model. A user can give a rating according to six different criteria -- cleanliness, communication, check_in, accuracy, location, and value -- which are defined as fields in the Rating model.
class Rating(models.Model):
cleanliness = models.PositiveIntegerField()
communication = models.PositiveIntegerField()
check_in = models.PositiveIntegerField()
accuracy = models.PositiveIntegerField()
location = models.PositiveIntegerField()
value = models.PositiveIntegerField()
class Review(models.Model):
room = models.ForeignKey('room.Room', on_delete=models.SET_NULL, null=True)
host = models.ForeignKey('user.User', on_delete=models.CASCADE, related_name='host_reviews')
guest = models.ForeignKey('user.User', on_delete=models.CASCADE, related_name='guest_reviews')
rating = models.OneToOneField('Rating', on_delete=models.SET_NULL, null=True)
content = models.CharField(max_length=2000)
I am thinking of a way to calculate the overall rating, which would be the average of average of each column in the Rating model. One way could be using Django's aggregate() function, and another option could be prefetching all reviews and looping through each review to manually calculate the overall rating.
For example,
for room in Room.objects.all()
ratings_dict = Review.objects.filter(room=room)\
.aggregate(*[Avg(field) for field in ['rating__cleanliness', 'rating__communication', \
'rating__check_in', 'rating__accuracy', 'rating__location', 'rating__value']])
ratings_sum = 0
for key in ratings_dict.keys():
ratings_sum += ratings_dict[key] if ratings_dict[key] else 0
Or, simply looping through,
rooms = Room.objects.prefetch_related('review_set')
for room in rooms:
reviews = room.review_set.all()
ratings = 0
for review in reviews:
ratings += (review.rating.cleanliness + review.rating.communication + review.rating.check_in +
review.rating.accuracy + review.rating.location+ review.rating.value)/6
Which way would be more efficient in terms of time complexity and result in less DB calls?
Does aggregate(Avg('field_name')) produce one Avg query at the database level per function call?
Will first calling all rooms with prefetch_related() help reduce number of queries later when calling room.review_set.all()?

Query to fetch highest rated movie with mimimum 5 people rated

I want to fetch name of movie with maximum rated movie with minimum 5 people rated in django.
My code :
model.py
class Movie(models.Model):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
vote_count = models.IntegerField()
class Watchlist(models.Model):
userid = models.IntegerField()
movie_id = models.ForeignKey(Movie, on_delete=models.CASCADE)
rating = models.IntegerField()
what will be query to get movie with highest rating with minimum 5 people ?
I propose that you make some changes to your model. Normally ForeignKeys do not end with an id suffix, since Django will add a "twin field" with an _id suffix that stores the value of the target field. Furthermore you probably better make a ForeignKey to the user model. If you do not specify a primary key yourself, Django will automatically add an field named id that is an AutoField, hendce there is no need to add that manually. Finally you do not need to store the vote_count in a field of the Movie, you can retrieve that by counting the number of related Rating objects:
from django.conf import settings
class Movie(models.Model):
title = models.CharField(max_length=100)
class Rating(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete.models.CASCADE)
movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
rating = models.IntegerField()
Then we can retrieve the highest rated movie with:
from django.db.models import Avg, Count
higest_rated = Movie.objects.annotate(
rating=Avg('rating__rating'),
votes=Count('rating')
).filter(votes__gte=5).order_by('-rating').first()
Here the votes__gte=5 will filter such that it will only obtain Movies with five or more votes, and we order by rating in descending order.
I'd modify the model, moving out Rating entity related fields from Watchlist and Movie.
Add the "Rate" class, and then filter by two conditions:
Count(Rate for the exact Movie) > minimum threshold(e.g. 5)
AVG(rating score for the exact Movie) > minimum threshold(e.g. 5)
or, if you need top-rated movies, use Order by as it described in that answer
In your case, you could use Count and Average with Watchlist.Rating field

Django models - Able to dynamically calculate the value of a field

Say for example I have a rating which is attached to a product model like so..
class Product(models.Model):
...
rating = models.IntegerField(...)
I want the product rating to change as new Reviews (that include a star rating of the product) change or updated/deleted.
class Review(models.Model):
...
related_product = models.ForeignKey(Product, on_delete=models.CASCADE, ...)
rating = models.IntegerField(...)
Initially, I used a method on the product class to calculate the value of rating by counting the rating value from each review and then dividing by the number of reviews to get an average.
class Product(models.Model):
...
def rating(self):
total_rating = ...
reviews_count = ...
average = total_rating / reviews_count
However, this doesn't allow me to use order_by('rating') when querying the objects and sending back the objects by order of their rating since 'rating' has to be defined in the database (i.e. as a field instead of a method).
Is there any way that I can calculate the value for rating which is then stored in the database?
You can annotate your queryset to calculate the average using your database
from django.db.models import Avg
Product.objects.all().annotate(
rating=Avg('review_set__rating')
).order_by('rating')
Now each Product object in the queryset will have an additional attribute rating
If you must store this rating because it becomes expensive to calculate then #niklas' comment is the correct solution

How can I access a specific field of a queryset in Django?

I have these models in my Django app:
class Book(models.Model):
title = models.CharField(max_length=50, unique=True)
owner = models.CharField(max_length=30)
price = models.DecimalField(max_digits=5, decimal_places=2, null=True)
book_rating = models.ForeignKey('Rating', null=True)
RATE_CHOICES = zip(range(1,6), range(1,6))
class Rating(models.Model):
user = models.ForeignKey(User)
this_book = models.ForeignKey(Book)
rate = models.DecimalField(max_digits=2, decimal_places=1, choices=RATE_CHOICES)
comment = models.TextField(max_length=4000, null=True)
I am trying to access the Ratings of each instance of the Book model. Here is what I've tried so far in the shell:
from django.contrib.contenttypes.models import ContentType
>>> ctype = ContentType.objects.get_for_model(Rating)
>>> ctype
<ContentType: rating>
>>> book_titles = ctype.model_class().objects.filter(this_book__title='My Test Book')
>>> book_titles
<QuerySet [<Rating: My Test Book - parrot987 - 3.0>, <Rating: My Test Book - 123#gmail.com - 5.0>]>
How can I access the two rating values of each object (5.0 and 3.0) without all of this other data?
Can this be done in such a way that I am able to average the numbers and return the final value?
For 1. you can use (relevant documentation):
Rating.objects.filter(this_book__title='My Test Book').values('rate')
If you just want a flat list you can use values_list('rate', flat=True) instead of values('rate').
For 2 (relevant documentation):
from django.db.models import Avg
Rating.objects.filter(this_book__title='My Test Book').aggregate(Avg('rate'))
This will return a dictionary where the key is rate__avg and the value is the average of the ratings.
Please see the following for Many to One fields django - Get the set of objects from Many To One relationship
To access the rating, you can use a for loop and access the individual values e.g.
total = 0
for rating in book_titles.book_set.all()
total += rating.rate
Good luck!

Categories