From 82e7690655bb5ab2a4e7bcf27e6b1fe4d06dec4b Mon Sep 17 00:00:00 2001 From: ilia Date: Sun, 21 May 2023 17:13:16 +0300 Subject: [PATCH] add personal recommendations --- config/api_router.py | 3 +- passfinder/recomendations/admin.py | 3 +- passfinder/recomendations/api/views.py | 48 +++++- ..._lays_userpreferences_unpreffered_plays.py | 18 +++ passfinder/recomendations/models.py | 2 +- passfinder/recomendations/service/service.py | 147 +++++++++++++++++- 6 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 passfinder/recomendations/migrations/0002_rename_unpreffered_lays_userpreferences_unpreffered_plays.py diff --git a/config/api_router.py b/config/api_router.py index fb5f751..58beb38 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -1,10 +1,11 @@ from rest_framework.routers import DefaultRouter -from passfinder.recomendations.api.views import TinderView +from passfinder.recomendations.api.views import TinderView, PersonalRecommendation router = DefaultRouter() router.register('tinder', TinderView) +router.register("recommendations", PersonalRecommendation) app_name = "api" urlpatterns = router.urls \ No newline at end of file diff --git a/passfinder/recomendations/admin.py b/passfinder/recomendations/admin.py index 8c38f3f..d903cdc 100644 --- a/passfinder/recomendations/admin.py +++ b/passfinder/recomendations/admin.py @@ -1,3 +1,4 @@ from django.contrib import admin +from .models import UserPreferences -# Register your models here. +admin.site.register([UserPreferences]) \ No newline at end of file diff --git a/passfinder/recomendations/api/views.py b/passfinder/recomendations/api/views.py index e0d9725..eb39b0c 100644 --- a/passfinder/recomendations/api/views.py +++ b/passfinder/recomendations/api/views.py @@ -8,6 +8,11 @@ from rest_framework.decorators import action from rest_framework.response import Response from .serializers import TinderProceedSerializer +from passfinder.recomendations.models import UserPreferences +from ..service.service import update_preferences_state, get_next_tinder, get_personal_concerts_recommendation, \ + get_personal_plays_recommendation, get_personal_movies_recommendation +from django.views.decorators.csrf import csrf_exempt + class TinderView(viewsets.GenericViewSet): @@ -17,10 +22,47 @@ class TinderView(viewsets.GenericViewSet): @action(methods=['GET'], detail=False, serializer_class=EventSerializer) def start(self, request: Request, *args: Any, **kwargs: Any): + UserPreferences.objects.get_or_create(user=request.user) event = EventSerializer(choice(Event.objects.all())) return Response(data=event.data, status=200) + @csrf_exempt @action(methods=['POST'], detail=True, serializer_class=TinderProceedSerializer) - def proceed(self, request: Request, *args: Any, **kwargs: Any): - event = EventSerializer(choice(Event.objects.all())) - return Response(data={'event': event.data}, status=200) + def proceed(self, request: Request, pk): + update_preferences_state(request.user, Event.objects.get(oid=pk), request.data['action']) + event = get_next_tinder(request.user, Event.objects.get(oid=pk), request.data['action']) + if event is None: + return Response(data={}, status=404) + return Response(data={'event': EventSerializer(event).data}, status=200) + + +class PersonalRecommendation(viewsets.GenericViewSet): + serializer_class = EventSerializer + model = Event + queryset = Event.objects.all() + + @action(methods=['GET'], detail=False) + def plays(self, request, *args, **kwargs): + recs = get_personal_plays_recommendation(request.user) + ans = [] + for rec in recs: + ans.append(EventSerializer(rec[1]).data) + return Response(ans, 200) + + + @action(methods=['GET'], detail=False) + def concerts(self, request, *args, **kwargs): + recs = get_personal_concerts_recommendation(request.user) + ans = [] + for rec in recs: + ans.append(EventSerializer(rec[1]).data) + return Response(ans, 200) + + + @action(methods=['GET'], detail=False) + def movies(self, request, *args, **kwargs): + recs = get_personal_movies_recommendation(request.user) + ans = [] + for rec in recs: + ans.append(EventSerializer(rec[1]).data) + return Response(ans, 200) \ No newline at end of file diff --git a/passfinder/recomendations/migrations/0002_rename_unpreffered_lays_userpreferences_unpreffered_plays.py b/passfinder/recomendations/migrations/0002_rename_unpreffered_lays_userpreferences_unpreffered_plays.py new file mode 100644 index 0000000..42ef53f --- /dev/null +++ b/passfinder/recomendations/migrations/0002_rename_unpreffered_lays_userpreferences_unpreffered_plays.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-05-21 10:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("recomendations", "0001_initial"), + ] + + operations = [ + migrations.RenameField( + model_name="userpreferences", + old_name="unpreffered_lays", + new_name="unpreffered_plays", + ), + ] diff --git a/passfinder/recomendations/models.py b/passfinder/recomendations/models.py index bfd0eef..831ced3 100644 --- a/passfinder/recomendations/models.py +++ b/passfinder/recomendations/models.py @@ -7,7 +7,7 @@ class UserPreferences(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) preffered_plays = models.ManyToManyField(Event, related_name='preffered_user_play') - unpreffered_lays = models.ManyToManyField(Event, related_name='unpreffered_users_play') + unpreffered_plays = models.ManyToManyField(Event, related_name='unpreffered_users_play') preffered_movies = models.ManyToManyField(Event, related_name='preffered_user_movie') unpreffered_movies = models.ManyToManyField(Event, related_name='unpreffered_user_movie') diff --git a/passfinder/recomendations/service/service.py b/passfinder/recomendations/service/service.py index e7f7b43..3880251 100644 --- a/passfinder/recomendations/service/service.py +++ b/passfinder/recomendations/service/service.py @@ -2,6 +2,9 @@ from .mapping.mapping import * from .models.models import * from passfinder.events.models import Event +from passfinder.recomendations.models import UserPreferences +from random import choice +from collections import Counter def get_nearest_(instance_model, model_type, mapping, nearest_n, ml_model): @@ -28,7 +31,7 @@ def nearest_movie(movie, nearest_n): def nearest_plays(play, nearest_n): - return get_nearest_(play, 'play', plays_mapping, nearest_n, plays_model) + return get_nearest_(play, 'plays', plays_mapping, nearest_n, plays_model) def nearest_excursion(excursion, nearest_n): @@ -37,3 +40,145 @@ def nearest_excursion(excursion, nearest_n): def nearest_concert(concert, nearest_n): return get_nearest_(concert, 'concert', concert_mapping, nearest_n, concert_model) + + +def get_nearest_event(event, nearest_n): + if event.type == 'plays': + return nearest_plays(event, nearest_n) + if event.type == 'concert': + return nearest_concert(event, nearest_n) + if event.type == 'movie': + return nearest_movie(event, nearest_n) + + +def update_preferences_state(user, event, direction): + pref = UserPreferences.objects.get(user=user) + + if direction == 'left': + if event.type == 'plays': + pref.unpreffered_plays.add(event) + if event.type == 'movie': + pref.unpreffered_movies.add(event) + if event.type == 'concert': + pref.unpreferred_concerts.add(event) + else: + if event.type == 'plays': + pref.preffered_plays.add(event) + if event.type == 'movie': + pref.preffered_movies.add(event) + if event.type == 'concert': + pref.preferred_concerts.add(event) + pref.save() + + + +def get_next_tinder(user, prev_event, prev_direction): + pref = UserPreferences.objects.get(user=user) + print(prev_event.type, len(pref.preferred_concerts.all())) + if prev_direction == 'left': + if prev_event.type == 'plays' and len(pref.unpreffered_plays.all()) <= 2: + candidates = nearest_plays(prev_event, 100) + # print(candidates, type(candidates), len(Event.objects.filter(type='plays'))) + return candidates[-1] + if prev_event.type == 'movie' and len(pref.unpreffered_movies.all()) <= 2: + candidates = nearest_movie(prev_event, 100) + return candidates[-1] + if prev_event.type == 'concert' and len(pref.unpreferred_concerts.all()) <= 2: + candidates = nearest_concert(prev_event, 100) + return candidates[-1] + + if prev_direction == 'right': + if prev_event.type == 'plays' and len(pref.preffered_plays.all()) < 2: + candidates = nearest_plays(prev_event, 2) + return candidates[1] + if prev_event.type == 'movie' and len(pref.preffered_movies.all()) < 2: + candidates = nearest_movie(prev_event, 2) + return candidates[1] + if prev_event.type == 'concert' and len(pref.preferred_concerts.all()) < 2: + candidates = nearest_concert(prev_event, 2) + return candidates[1] + + if prev_event.type == 'plays': + if not len(pref.preffered_movies.all()) and not len(pref.unpreffered_movies.all()): + return choice(Event.objects.filter(type='movie')) + if not len(pref.preferred_concerts.all()) and not len(pref.unpreferred_concerts.all()): + return choice(Event.objects.filter(type='concert')) + + if prev_event.type == 'movie': + if not len(pref.preffered_plays.all()) and not len(pref.unpreffered_plays.all()): + return choice(Event.objects.filter(type='plays')) + if not len(pref.preferred_concerts.all()) and not len(pref.unpreferred_concerts.all()): + return choice(Event.objects.filter(type='concert')) + + if prev_event.type == 'concert': + if not len(pref.preffered_plays.all()) and not len(pref.unpreffered_plays.all()): + return choice(Event.objects.filter(type='plays')) + if not len(pref.preffered_movies.all()) and not len(pref.unpreffered_movies.all()): + return choice(Event.objects.filter(type='movie')) + + return None + + + +def rank_candidates(candidates_list, negative_candidates_list): + flatten_c_list = [] + ranks = {} + + flatten_negatives = [] + + for negative in negative_candidates_list: + flatten_negatives.extend(negative) + + for lst in candidates_list: + flatten_c_list.extend(lst) + for cand in lst: + ranks.update({cand: {'rank': 0, 'lst': lst}}) + + cnt = Counter(flatten_c_list) + + for candidate, how_many in cnt.most_common(len(flatten_c_list)): + ranks[candidate]['rank'] = how_many * (len(ranks[candidate]['lst']) - ranks[candidate]['lst'].index(candidate)) + + res = [] + for cand in ranks.keys(): + res.append((ranks[cand]['rank'], cand)) + return list(filter(lambda x: x[1] not in flatten_negatives, sorted(res, key=lambda x: -x[0]))) + + +def get_personal_recommendation(prefer, unprefer): + candidates = [] + negative_candidates = [] + + for rec in prefer: + candidates.append(list(map(lambda x: x.oid, get_nearest_event(rec, 10)[1:]))) + + for neg in unprefer: + negative_candidates.append(list(map(lambda x: x.oid, get_nearest_event(neg, 10)[1:]))) + + ranked = rank_candidates(candidates, negative_candidates) + + return list(map(lambda x: (x[0], Event.objects.get(oid=x[1])), ranked[0:5])) + + +def get_personal_plays_recommendation(user): + pref = UserPreferences.objects.get(user=user) + + prefer = pref.preffered_plays.all() + unprefer = pref.unpreffered_plays.all() + return get_personal_recommendation(prefer, unprefer) + + +def get_personal_concerts_recommendation(user): + pref = UserPreferences.objects.get(user=user) + + prefer = pref.preferred_concerts.all() + unprefer = pref.unpreferred_concerts.all() + return get_personal_recommendation(prefer, unprefer) + + +def get_personal_movies_recommendation(user): + pref = UserPreferences.objects.get(user=user) + + prefer = pref.preffered_movies.all() + unprefer = pref.unpreffered_movies.all() + return get_personal_recommendation(prefer, unprefer)