본문 바로가기
DataScience/머신러닝

Web 개발 :: 프로젝트 조회수, Permission, Dataframe, 머신러닝_TIL66

by 올커 2022. 12. 7.

■ JITHub 개발일지 66일차

□  TIL(Today I Learned) ::

프로젝트 조회수, Permission, Dataframe, 머신러닝

   1) 게시글 조회수 생성

       조회수 생성은 아래와 같은 코드로 간단하게 적용할 수 있었다.

class Place(models.Model):
    ...
    hit = models.PositiveIntegerField('조회수', default=0)
    ...

    @property
    def hit_count(self):
        self.hit +=1
        self.save()

  - 실제 코드를 실행할 때에는 views.py의 함수에서 위의 hit_count를 호출해와야 동작이 되는데 이를 호출해오지 않아 hit count가 제대로 되지 않았던 문제가 있었다.

  - 아래 코드처럼 place.hit_count를 추가해주면서 문제를 해결할 수 있었다.

#views.py

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)

   2) 장소(Place) DATA CRUD 생성(Admin권한으로만 Create, Update, Delete 접근)

      - 장소 데이터는 Admin권한을 가질 경우에만 Create, Update, Delete에 접근할 수 있도록 하려 한다.

      - 아래와 같이 permissions.py를 지정해준다. 여기서 IsAdmin 클래스의 has_permission 함수를 호출해와서 사용하게 된다.

 

# permissions.py

from rest_framework.permissions import BasePermission
from rest_framework.exceptions import APIException
from rest_framework import status


class GenericAPIException(APIException):
    def __init__(self, status_code, detail=None, code=None):
        self.status_code=status_code
        super().__init__(detail=detail, code=code)

class IsAdmin(BasePermission):
    """
    admin 사용자는 모든request 가능,
    비로그인, 로그인한 사람은 조회만 가능
    """
    SAFE_METHODS = ('GET', )
    message = '접근 권한이 없습니다.'

    def has_permission(self, request, view):
        user = request.user
        if request.method in self.SAFE_METHODS:
            return True

        if not user.is_authenticated or user.is_anonymous:
            response = {
                'detail': "관리자만 접근이 가능합니다."
            }
            raise GenericAPIException(status_code=status.HTTP_403_FORBIDDEN, detail=response)
        
        if user.is_admin and user.is_authenticated:
            return True

        if (user.is_anonymous or user.is_authenticated) and request.method in self.SAFE_METHODS:
            return True

        return False

 

  - View에서 아래와 같이 permission_classes = [IsAdmin]과 같이 적어주면 함수를 호출할 수 있다.

  (※ 여기서 문제가 되었던 부분은 pwermissions_classes 라고 's' 한글자 Typo 때문에 실행이 안되는 문제가 있었다.)

##### 장소 #####
class PlaceListView(APIView):
    permission_classes = [IsAdmin]

    #맛집 리스트
    def get(self, request):
        place = Place.objects.all()
        serializer = PlaceSerializer(place, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

    #맛집 생성
    def post(self, request):
        serializer = PlaceCreateSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

   3) 장소 선택 데이터 GET

      - 유저가 초기에는 경험데이터(리뷰 데이터)가 없기 때문에 비로그인 접속자나, 신규 회원에게는 선호도를 받고자 했다.

      - 장소 데이터는 직접 정한 카테고리를 이용하기 위해 FOOD_CATEGORY를 만들어주고, 튜플 안을 돌면서 카테고리 이름을 가져오기 위해 for문과 Place.objects.filter를 사용했다. order_by는 기존에 갖고 있던 네이버 평점데이터 'rating'을 받아오기 위해 추가해주었다. 각각의 pick은 place에 넣어주고 결과값을 serializer를 통해 Response로 전달해준다.

FOOD_CATEGORY = (
        ('F1', '분식'),
        ('F2', '한식'),
        ('F3', '돼지고기구이'),
        ('F4', '치킨,닭강정'),
        ('F5', '햄버거'),
        ('F6', '피자'),
        ('F7', '중식당'),
        ('F8', '일식당'),
        ('F9', '양식'),
        ('F10', '태국음식'),
        ('F11', '인도음식'),
        ('F12', '베트남음식'),
    )

##### 취향 선택 #####
class PlaceSelectView(APIView):
    permission_class = [IsAuthenticated]

    #맛집 취향 선택
    def get(self, request):
        place = []
        for i in range(len(FOOD_CATEGORY)):
            pick = Place.objects.filter(category=FOOD_CATEGORY[i][1]).order_by('-rating')[0]
            place.append(pick)
        serializer = PlaceSelectSerializer(place, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

   4) 머신러닝 장소 추천 기능

      - 처음에 DB에서 데이터를 가져와서 데이터프레임으로 어떻게 만들지 고민을 많이했다. 생각보다 코드는 간단히 사용할 수 있었다. 앞으로도 필요하다면 이 코드를 참고하면 좋을 듯 하다.

pd.DataFrame(list(Place.objects.values()))

      - dataframe의 칼럼 아이디도 아래와 같은 코드로 간단히 변경이 가능하다.

#Dataframe의 칼럼 아이디를 변경 {<기존>:<변경>}
places.rename(columns={'id':'place_id'}, inplace=True)

 

import pandas as pd
import numpy as np

from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import CountVectorizer

import json

import sys
import os
import django

BASE_DIR = os.path.dirname(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

#DB에서 데이터를 가져와서 Dataframe으로 만들어주기
places = pd.DataFrame(list(Place.objects.values()))
reviews = pd.DataFrame(list(Review.objects.values()))

#Dataframe의 칼럼 아이디를 변경 {<기존>:<변경>}
places.rename(columns={'id':'place_id'}, inplace=True)

# print(reviews.head)
# print(places.head)

#places, reviews 두 개의 dataframe 병합
place_ratings = pd.merge(places, reviews, on='place_id')
# print(place_ratings.head)

# user별로 Place에 부여한 rating을 볼 수 있도록 pivot table사용
title_user = place_ratings.pivot_table('rating_cnt', index='author_id', columns='place_id')

#rating을 부여안한 장소는 3으로 부여
title_user = title_user.fillna(3)
# print(title_user.head)

# User간 Cosine Similarity를 numpy 행렬로 변환
user_sim_np = cosine_similarity(title_user, title_user)
user_sim_df = pd.DataFrame(user_sim_np, index=title_user.index, columns=title_user.index)
print(user_sim_df.head)

# 1번 유저와 비슷한 유저를 내림차순으로 정렬 후 상위 10개만 뽑음
print(user_sim_df[1].sort_values(ascending=False)[:10])

# 1번 유저와 가장 비슷한 유저를 뽑고
user = user_sim_df[1].sort_values(ascending=False)[:10].index[1]
# 가장 비슷한 유저가 평점을 높이 준 장소를 내림차순으로 정렬
result = title_user.query(f"author_id == {user}").sort_values(ascending=False, by=user, axis=1).transpose()[:10]
print(result)

  - 위의 머신러닝 테스트 코드를 작성한 후 다른 파일에서 호출해서 사용하기 위해 아래와 같이 코드를 변경하였다.

  - 필요한 정보로는 user_id, picked_place_id를 받아온다.

  - dataframe을 수정해주기 위해 df.loc[x] = np.nan(x행을 신규로 생성하여 NaN값으로 만들어주기), df.loc[x, y] = z(x행 y열의 값을 z로 삽입하기) 등 데이터프레임 관련 함수들을 추가로 알아봐야 할 필요가 있었다.

# rcm_places.py (1207_1차 완료 코드)

import pandas as pd
import numpy as np

from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import CountVectorizer

import json

import sys
import os
import django

BASE_DIR = os.path.dirname(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

def rcm_place(user_id, picked_place_id):
    places = pd.DataFrame(list(Place.objects.values()))
    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')
    review_user = review_user.fillna(0)

    review_user.loc[user_id] = np.nan
    review_user = review_user.fillna(0)
    review_user.loc[user_id, picked_place_id] = 5
    print(review_user)

    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)
    print(user_sim_df.head)
    print(user_sim_df[user_id].sort_values(ascending=False)[:])

    picked_user = user_sim_df[user_id].sort_values(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

  - 관련함수들이 너무나 많아 기억하기 쉽지않을 것 같아 아래와 같이 정리된 자료가 있어 스크랩해 정리했다.

    (*참고 링크)

import pandas as pd

#빈 DataFrame 생성
df1 = pd.DataFrame(columns=range(5))

#행 추가
df1.loc[0]=[1,2,3,4,5]

#열 길이 같은 DataFrame 행 병합
df2 = pd.DataFrame(columns=range(5))
df2.loc[0]=[11,12,13,14,15]

df1 = df1.append(df2)


#행 길이 같은 DataFrame 열 병합
df3 = pd.DataFrame([[3],[4]],columns=range(5,6))

df1 = df1.join((df3))


#행 이름 설정
df1.index = range(1,3)

#열 이름 설정
df1.columns =range(1,7)

#특정 행 이름 바꾸기
df1.rename(index={2:4},inplace=True)

#특정 열 이름 바꾸기
df1.rename(columns={6:7},inplace=True)

#빈 값으로 데이터 넣기
df1.loc[2]=[3,5,6,7,9,2]


import numpy as np
df1[6] = np.nan
df1.loc[3]=np.nan

#행 순서 바꾸기
df1 = df1.reindex(index=[1,2,3,4])

#열 순서 바꾸기
df1=df1[list(range(1,8))]#df1[[1,2,3,4,5,6,7]]

#특정값 변경
df.loc[2, 'A'] = 3000

   5) 여러 개의 필터 조건 넣기

      - <model_name>.objects.filter는 일반적으로 하나의 조건을 가져온다고 알고있었는데...

      - 아래와 같이 filter(id__in=<리스트>)를 넣으면 두 개 이상의 조건대로 모델에서 데이터를 가져올 수 있다는 사실을 새로 알게 되었다.

#places/views.py

class PlaceListView(APIView):
    permission_classes = [IsAdminOrOntherReadOnly]

    #맛집 전체 리스트
    @swagger_auto_schema(operation_summary="맛집 전체 리스트",
                    responses={200 : '성공', 500 : '서버 에러'})
    #맛집 리스트
    def get(self, request, place_id):
        place_list = rcm_place(user_id = request.user.id, picked_place_id=place_id)
        place = Place.objects.filter(id__in=place_list)
        serializer = PlaceSerializer(place, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)
반응형

댓글