■ JITHub 개발일지 79일차
□ TIL(Today I Learned) ::
Code Review _ Place 추천 기능
Place App 기능 페이지
1) index.html
- 카테고리에서 음식, 장소 선택
2) place_preference.html
- 활성화 : 유저 경험 데이터가 없는 유저(비로그인 계정, 리뷰가 없는 계정)
(1) 음식 종류(한식/분식, 패스트푸드, 중식, …)
(2) 장소(제주시, 서귀포시)
3) place_list
- 조건에 맞는 추천 결과를 보여주는 화면
- 로그인(리뷰有) 유저 : index.html → place_list.html
- 비로그인(리뷰X) 유저 : index.html → place_preference.html → place_list.html
Place App Structure
├─ places
│ ├─ migrations
│ ├─ __init__.py
│ ├─ admin.py
│ ├─ apps.py
│ ├─ client.py
│ ├─ index.py
│ ├─ models.py ***
│ ├─ rcm_places.py ***
│ ├─ serializers.py ***
│ ├─ tests.py
│ ├─ urls.py ***
│ └─ views.py ***
urls.py
from django.urls import path
from . import views
urlpatterns = [
# Place
path('<int:place_id>/', views.PlaceDetailView.as_view(), name='place_detail_view'),
path('<int:place_id>/bookmarks/',views.PlaceBookmarkView.as_view(), name='place_bookmark_view'),
#Recommendation
path('selection/<int:choice_no>/', views.PlaceSelectView.as_view(), name="place_select_view"),
path('new/<int:place_id>/<str:category>/', views.NewUserPlaceListView.as_view(), name='new_user_place_list_view'),
path('list/<int:cate_id>/', views.UserPlaceListView.as_view(), name='user_place_list_view'),
# Search
path('search/', views.SearchListView.as_view(), name='search'),
]
Models.py
from django.db import models
from django.db.models import Q
from django.core.validators import MaxValueValidator
from users.models import User
...
class Place(models.Model):
place_name = models.CharField('장소명', max_length=50)
category = models.CharField('카테고리', max_length=20)
rating = models.DecimalField('별점', max_digits=3, decimal_places=2, default=0, validators=[MaxValueValidator(5)])
menu = models.TextField('메뉴', null=True)
place_desc = models.CharField('소개글', max_length=255, null=True)
place_address = models.CharField('주소', max_length=100)
place_number = models.CharField('장소 전화번호', max_length=20)
place_time = models.CharField('영업 시간', max_length=30)
place_img = models.TextField('장소 이미지', null=True)
latitude = models.CharField('위도', max_length=50, null=True)
longitude = models.CharField('경도', max_length=50, null=True)
hit = models.PositiveIntegerField('조회수', default=0)
place_bookmark = models.ManyToManyField(User, verbose_name='장소 북마크', related_name="bookmark_place",blank=True)
objects = PlaceManager()
class Meta:
db_table = 'places'
def __str__(self):
return f'[장소명]{self.place_name}'
@property
def hit_count(self):
self.hit +=1
self.save()
Serializers.py
from rest_framework import serializers
from .models import Place
#맛집 serializer
class PlaceSerializer(serializers.ModelSerializer):
class Meta:
model = Place
fields = ('id', 'place_name', 'category', 'rating', 'menu', 'place_desc', 'place_address', 'place_number', 'place_time', 'place_img', 'latitude', 'longitude', 'hit', 'place_bookmark', )
Views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.generics import get_object_or_404
from rest_framework.pagination import PageNumberPagination
from django.db.models import Case, When
from drf_yasg.utils import swagger_auto_schema
from gaggamagga.permissions import IsAdminOrOntherReadOnly
from gaggamagga.pagination import PaginationHandlerMixin
from . import client
from .models import Place
from reviews.models import Review
from .serializers import PlaceSerializer
from .rcm_places import rcm_place_user, rcm_place_new_user
import random
import pandas as pd
import numpy as np
CHOICE_ONE = ['분식', '한식', '돼지고기구이','치킨,닭강정', '햄버거', '피자', '중식', '일식', '양식', '태국음식', '인도음식', '베트남음식', '제주시', '서귀포시']
CHOICE_CATEGORY = (
('1', '분식'),
('2', '한식'),
('3', '돼지고기구이'), #-- 한식
('4', '치킨,닭강정'),
('5', '햄버거'),
('6', '피자'), #-- 패스트푸드
('7', '중식'),
('8', '일식'),
('9', '양식'),
('10', '태국음식'),
('11', '인도음식'),
('12', '베트남음식'), #-- 아시아
('13', '제주시'),
('14', '서귀포시'),
)
class PlaceListPagination(PageNumberPagination):
page_size = 10
1) 맛집 상세 페이지, 삭제
class PlaceDetailView(APIView):
permission_classes = [IsAdminOrOntherReadOnly]
# 맛집 상세 페이지
@swagger_auto_schema(operation_summary="맛집 상세 페이지",
responses={200 : '성공', 404 : '찾을 수 없음', 500 : '서버 에러'})
def get(self, request, place_id):
place = get_object_or_404(Place, id=place_id)
place.hit_count
serializer = PlaceSerializer(place)
return Response(serializer.data, status=status.HTTP_200_OK)
# 맛집 삭제
@swagger_auto_schema(operation_summary="맛집 삭제",
responses={200 : '성공', 401 : '인증 에러', 404 : '찾을 수 없음', 500 : '서버 에러'})
def delete(self, request, place_id):
place = get_object_or_404(Place, id=place_id)
place.delete()
return Response({"message":"맛집 삭제 완료"},status=status.HTTP_200_OK)
2) 맛집 취향 선택
class PlaceSelectView(APIView):
permission_classes = [AllowAny]
# 맛집 취향 선택(리뷰가 없거나, 비로그인 계정일 경우)
@swagger_auto_schema(operation_summary="맛집 취향 선택",
responses={200 : '성공', 500 : '서버 에러'})
def get(self, request, choice_no): # choice_no : index에서 선택했던 음식종류 or 장소(1~12, 13, 14)
load_no = random.randint(1, 6)
# Case1: choice place location
if choice_no > 12: # 제주시(13), 서귀포시(14) 장소 선택시
place_list = []
for i in range(0, 12): # contain을 통해 해당되는 장소의 쿼리셋을 가져오고, 그 중 카테고리별 필터링한 값의 첫번째를 가져온다.
pick = Place.objects.filter(place_address__contains=CHOICE_CATEGORY[choice_no-1][1],category=CHOICE_CATEGORY[i][1]).first()
if pick == None: # 해당되는 카테고리가 없다면 pass
pass
else: # 해당되는 카테고리가 있다면 id를 place_list에 추가한다.
place_list.append(pick.id)
# Create list for custom sorting
# for문을 통해 뽑아낸 리스트의 순서를 그대로 저장하기위해 아래
preserved = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(place_list)])
place = Place.objects.filter(id__in=place_list).order_by(preserved)
serializer = PlaceSerializer(place, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
# Case2: choice food group
else:
if (choice_no == 3)|(choice_no == 6)|(choice_no == 12): #(한식, 패스트푸드, 아시아)
# Merge queryset for 3categories
place_list = []
pick1 = Place.objects.filter(category=CHOICE_CATEGORY[choice_no-1][1])[load_no-1:load_no+2]
pick2 = Place.objects.filter(category=CHOICE_CATEGORY[choice_no-2][1])[load_no-1:load_no+2]
pick3 = Place.objects.filter(category=CHOICE_CATEGORY[choice_no-3][1])[load_no-1:load_no+2]
pick = (pick1|pick2|pick3) # 쿼리셋 병합
serializer = PlaceSerializer(pick, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
else: #(그외)
place_list = []
pick = Place.objects.filter(category=CHOICE_CATEGORY[choice_no-1][1])[0:9]
serializer = PlaceSerializer(pick, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
when_cases = []
for pos, pk in enumerate(place_list):
when_cases.append(When(pk=pk, then=pos))
preserved = Case(*when_cases)
>>> *(asterisk)를 사용한 Unpacking
Case(When(...), When(...), When(...), ...)
3) 맛집 추천
##### 맛집(리뷰가 없거나, 비로그인 계정일 경우) #####
class NewUserPlaceListView(PaginationHandlerMixin, APIView):
permission_classes = [AllowAny]
pagination_class = PlaceListPagination
# 맛집 리스트 추천
@swagger_auto_schema(operation_summary="맛집 리스트 추천(비유저)",responses={200 : '성공', 500 : '서버 에러'})
def get(self, request, place_id, category):
places = pd.DataFrame(list(Place.objects.values()))
cate_id = CHOICE_ONE.index(category)+1
if cate_id <= 12: # Case1: choice food group
if (cate_id == 3)|(cate_id == 6)|(cate_id == 12): #(한식, 패스트푸드, 아시아)
category1 = CHOICE_CATEGORY[cate_id-1][1] 분식
category2 = CHOICE_CATEGORY[cate_id-2][1] 한식
category3 = CHOICE_CATEGORY[cate_id-3][1] 돼지고기구이
place1 = places[places['category'].str.contains(category1)]
place2 = places[places['category'].str.contains(category2)]
place3 = places[places['category'].str.contains(category3)]
place_list = [place1, place2, place3]
places = pd.concat(place_list, ignore_index=True) # 데이터 프레임 병합, 기존 index 무시
else:
cate = CHOICE_CATEGORY[cate_id-1][1]
places = places[places['category'].str.contains(cate)]
else: # Case2: choice place location
cate = CHOICE_CATEGORY[cate_id-1][1]
places = places[places['place_address'].str.contains(cate)]
# Create dataframe
reviews = pd.DataFrame(list(Review.objects.values()))
places.rename(columns={'id':'place_id'}, inplace=True) # reviews 데이터 프레임과 컬럼 이름 맞추기
place_ratings = pd.merge(places, reviews, on='place_id') # places와 reviews를 하나의 데이터프레임으로 병합
review_user = place_ratings.pivot_table('rating_cnt', index='author_id', columns='place_id') # 피봇테이블 생성
place_list = rcm_place_new_user(review_user=review_user, place_id=place_id) # 머신러닝(코사인 유사도) 실행
preserved = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(place_list)])
place = Place.objects.filter(id__in=place_list).order_by(preserved) # place_list의 id 포함조건을 or로 줌, preserverd에 따라 정렬
page = self.paginate_queryset(place) # 페이지네이션 실행
serializer = self.get_paginated_response(PlaceSerializer(page, many=True).data)
return Response(serializer.data, status=status.HTTP_200_OK)
##### 맛집(유저일 경우) #####
class UserPlaceListView(PaginationHandlerMixin, APIView):
permission_classes = [IsAuthenticated]
pagination_class = PlaceListPagination
# 맛집 리스트 추천
@swagger_auto_schema(operation_summary="맛집 리스트 추천(유저)",
responses={200 : '성공', 401 : '인증 에러', 500 : '서버 에러'})
def get(self, request, cate_id):
places = pd.DataFrame(list(Place.objects.values()))
if cate_id <= 12: # Case1: choice food group
if (cate_id == 3)|(cate_id == 6)|(cate_id == 12):
category1 = CHOICE_CATEGORY[cate_id-1][1]
category2 = CHOICE_CATEGORY[cate_id-2][1]
category3 = CHOICE_CATEGORY[cate_id-3][1]
place1 = places[places['category'].str.contains(category1)]
place2 = places[places['category'].str.contains(category2)]
place3 = places[places['category'].str.contains(category3)]
place_list = [place1, place2, place3]
places = pd.concat(place_list, ignore_index=True)
else:
category = CHOICE_CATEGORY[cate_id-1][1]
places = places[places['category'].str.contains(category)]
else: # Case2: choice place location
category = CHOICE_CATEGORY[cate_id-1][1]
places = places[places['place_address'].str.contains(category)]
# Create dataframe
reviews = pd.DataFrame(list(Review.objects.values()))
places.rename(columns={'id':'place_id'}, inplace=True)
place_ratings = pd.merge(places, reviews, on='place_id')
review_user = place_ratings.pivot_table('rating_cnt', index='author_id', columns='place_id')
if (request.user.id not in review_user.index): # 리뷰 데이터에 해당 유저가 없는 경우
col = random.choice(review_user.columns.to_list()) # .to_list 리스트타입으로 변형한 후 랜덤 column 뽑기
review_user.loc[request.user.id] = np.nan # 해당 유저 아이디를 na로 데이터열을
review_user.loc[request.user.id, col] = 5 # 위에서 뽑은 column에 5점 주기
review_user = review_user.fillna(0) # na로 비어있는 값들은 0으로 변환
place_list = rcm_place_user(review_user=review_user, user_id = request.user.id)
preserved = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(place_list)])
place = Place.objects.filter(id__in=place_list).order_by(preserved)
page = self.paginate_queryset(place)
serializer = self.get_paginated_response(PlaceSerializer(page, many=True).data)
return Response(serializer.data, status=status.HTTP_200_OK)
rcm_places.py (머신러닝 추천시스템)
- 유사도 측정 방식 : Cosine Similarity 채택
- 협업 필터링 추천 : ‘특정 인자(맛집)에 대한 선호도가 유사한 유저는 다른 인자(맛집)에 대해서도 선호도가 비슷할 것이다’ 라고 가정
- 사용자의 아이템 평가 데이터를 이용해 비슷한 선호도를 갖는 다른 사용자가 선택한 아이템 추천
- 환경 셋팅
import pandas as pd
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
import sys
import os
import django
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gaggamagga.settings')
django.setup()
from reviews.models import Review
from places.models import Place
import random
- django startapp을 통해 만든 파일이 아닌 곳에서 models를 불러오기 위해서 django 사용환경을 설정해주어야 했다.
(1) BASE_DIR = os.path.dirname(os.path.abspath(file)) (파일 경로 변수에 저장)
(2) sys.path.append (해당파일 경로에 환경변수 추가)
(3) os.environ.setdefault (환경변수 setting)
(4) django.setup() (django 환경 로드)
# 유사한 유저 정보 조회 및 추천(기존 사용이력이 없는 사용자)
def rcm_place_new_user(review_user, place_id):
# 유저 데이터 데이터 프레임 생성
new_idx = int(review_user.iloc[len(review_user)-1].name)+1 # 데이터프레임의 가장 마지막 레코드에 유저 추가
review_user.loc[new_idx] = np.nan
review_user.loc[new_idx, place_id] = 5
review_user = review_user.fillna(0)
# 코사인 유사도 분석
user_sim_np = cosine_similarity(review_user, review_user) # 만들어진 review_user를 활용해 cosine 유사도 분석
user_sim_df = pd.DataFrame(user_sim_np, index=review_user.index, columns=review_user.index) # 분석 결과를 Dataframe으로 변환
# 유사한 유저 선택
picked_user = user_sim_df.sort_values(by=new_idx, ascending=False).index[1] # sort_values를 통해 내림차순 정렬 후 가장 유사한 유저 pick
result = review_user.query(f"author_id == {picked_user}").sort_values(ascending=False, by=picked_user, axis=1) # pick한 유저가 맛집별로 준 rating값 내림차순으로 가져오기
result_list = []
for column in result:
result_list.append(column) # 위에서 가져온 rating값을 리스트에 넣고 리턴
return result_list
# 유사한 유저 정보 조회 및 추천(기존 유저)
def rcm_place_user(review_user, user_id):
# 코사인 유사도 분석
user_sim_np = cosine_similarity(review_user, review_user)
user_sim_df = pd.DataFrame(user_sim_np, index=review_user.index, columns=review_user.index)
# 유사한 유저 선택
picked_user = user_sim_df.sort_values(by=user_id, ascending=False).index[1]
result = review_user.query(f"author_id == {picked_user}").sort_values(ascending=False, by=picked_user, axis=1)
# 가장 유사한 유저 추천
result_list = []
for column in result:
result_list.append(column)
return result_list
pagination.py
from rest_framework.pagination import PageNumberPagination
class BasePagination(PageNumberPagination):
page_size_query_param = "page_size"
class PaginationHandlerMixin(object):
@property
def paginator(self):
if not hasattr(self, "_paginator"):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
else:
pass
return self._paginator
def paginate_queryset(self, queryset):
if self.paginator is None:
return None
return self.paginator.paginate_queryset(queryset, self.request, view=self)
def get_paginated_response(self, data):
assert self.paginator is not None
return self.paginator.get_paginated_response(data)
반응형
'DEV > Web 개발' 카테고리의 다른 글
Web개발 :: Code 기능 리뷰(검색), Deploy _TIL81 (1) | 2022.12.28 |
---|---|
Web개발 :: Code Review _ Review CRUD 기능 _TIL80 (1) | 2022.12.28 |
Web개발 :: Code Review _ User 관리 기능 _TIL78 (0) | 2022.12.28 |
Web 개발 :: 12월 넷째주 WIL17 (0) | 2022.12.28 |
Web개발 :: github action을 활용한 CI/CD _TIL77 (0) | 2022.12.28 |
댓글