API서버 - 실시간 추천 기능 구현하기 (상관계수 이용)

반응형

유저가 평점을 높게 남긴 영화들과 유사한 영화 실시간 추천해주기

  • 상관계수를 이용하여 상관계수가 높은 순으로 추천리스트 작성
  • 해당 기능만을 서술한 포스팅으로, 전체 기능에 대한 소스 코드 확인은 깃 허브에서 확인 가능합니다.
    https://github.com/luvris2/movie-api-server
 

GitHub - luvris2/movie-api-server

Contribute to luvris2/movie-api-server development by creating an account on GitHub.

github.com


Visual Studio Code

메인 파일 : app.py

  • 기능 : API 구축, 토큰 생성 및 관리, 리소스 경로 생성
from flask import Flask
from flask_jwt_extended import JWTManager
from flask_restful import Api

from ref.config import Config

from resources.recommend import MovieRecommendRealTimeResource

# API 서버를 구축하기 위한 기본 구조
app = Flask(__name__)

# 환경변수 셋팅
app.config.from_object(Config) # 만들었던 Config.py의 Config 클래스 호출

# JWT 토큰 생성
jwt = JWTManager(app)

# 로그아웃 된 토큰이 들어있는 set을 jwt에게 알림
@jwt.token_in_blocklist_loader
def check_it_token_is_revoked(jwt_header, jwt_payload):
    jti = jwt_payload['jti']
    return jti in jwt_blocklist

# restfulAPI 생성
api = Api(app)

# 경로와 리소스(api코드) 연결
api.add_resource(MovieRecommendRealTimeResource, '/movie_list/realtime_recommend')

if __name__ == '__main__' :
    app.run()

추가 리소스 파일 : resources/recommend.py

기능

  • 유저가 평점을 남긴 영화를 기준으로 가장 유사한 영화들을 추천해주는 시스템
  • 이름이 중복되는 영화 제목은 제외
  • 유저가 평점을 남긴 영화는 추천 목록에서 제외

기능 설계

기능 설계 1 - 전체 영화의 상관관계 계수 구하기

  • 전체 영화 리스트를 DB에서 확인하여 데이터프레임으로 저장 (movie_rating_df)
# 전체 영화 리스트 저장
query = ''' select r.user_id, r.movie_id, r.rating, m.title from rating r
            join movie m on r.movie_id = m.id; '''
cursor = connection.cursor(dictionary=True)
cursor.execute(query)
movie_list = cursor.fetchall()

# 전체 영화 리스트를 데이터프레임화
movie_rating_df = pd.DataFrame(data=movie_list)

 

  • 전체 영화간의 상관계수 피벗테이블로 저장 (df)
    • 평점을 기준으로 다른 영화와의 상관관계 확인
    • 별점 평가 50개 이상의 데이터 사용
# 피벗 테이블로 상관계수 출력, 50개 이상의 별점평가
corr_df = movie_rating_df.pivot_table(index='user_id', columns='title', values='rating', aggfunc='mean')
df = corr_df.corr(min_periods=50)

기능 설계 2 - 나의 평점 리스트에서 특정 영화의 상관관계 계수 구하기

  • 유저 식별 토큰을 받아 유저가 작성한 영화 평점 리스트를 DB에서 확인하여 데이터프레임으로 저장 (df_my_rating)
# 내가 남긴 평점 리스트 저장
# %s = user_id, 토큰화된 유저 식별 ID
query = ''' select r.user_id, r.movie_id, r.rating, m.title from rating r
            join movie m on r.movie_id = m.id and r.user_id = %s; '''
record = (user_id, )
cursor = connection.cursor(dictionary = True)
cursor.execute(query, record)
result_list = cursor.fetchall()

# 나의 평점 리스트를 데이터프레임화
df_my_rating = pd.DataFrame(data=result_list)

 

  • 평점을 남긴 영화와 다른 유저가 평점을 남긴 데이터를 토대로 다른 영화와의 상관관계를 구함
    • 상관관계는 내가 남긴 평점을 다른 계수와 곱함
      예) 기준이 되는 영화 5점, 상관계수 1 -> 5.0점
      예) 임의의 다른 영화의 상관계수 0.5 -> 2.5점
      > 상관계수가 5점에 가까울수록 관계가 있음. 즉, 유사하므로 추천
    • 유저 남긴 평점과 상관관계가 존재하는 영화들을 변수에 저장 (similar_movie_list)
# 내가 남긴 평점과 상관관계가 있는 영화 리스트를 저장할 데이터프레임 정의
similar_movie_list = pd.DataFrame()

# df_my_rating : 내가 남긴 평점 리스트
# df : 모든 영화와의 상관계수를 표현한 데이터프레임
# for : 모든 영화 리스트에서 내가 남긴 평점 리스트와 관련 있는 데이터 행만 저장하고 가중치 설정
for i in range( len(df_my_rating) ) :
    similar_movie = df[df_my_rating['title'][i]].dropna().sort_values(ascending=False).to_frame()
    similar_movie.columns= ['Correlations']
    similar_movie['Weight'] = df_my_rating['rating'][i] * similar_movie['Correlations']
    similar_movie_list = pd.concat([similar_movie_list, similar_movie])

 

  • 영화 이름 중복 제거
    • 동명의 영화가 존재 할 경우, 상관계수의 값이 2개 이상이므로 max로 설정
      • 동명의 영화의 상관계수는 기존의 영화보다 상관계수가 높을 수 없음
# 영화 이름은 같지만 다른 영화 존재시 상관계수 값이 두개 이상이 존재
# 상관계수의 최댓값은 1이므로 값이 1인 기존의 영화로 선택하는 과정
similar_movie_list = similar_movie_list.groupby('title')['Weight'].max().sort_values(ascending=False)

 

  • 내가 평가한 영화는 추천 목록에서 제외
    • 전체 영화 리스트에서 내가 평점을 내린 영화 리스트의 제목을 제거
# title_list : 내가 평점을 남긴 영화들을 리스트에 저장
# recommend_movie_list : 내가 평점을 남긴 영화들을 전체 영화 리스트에서 제거
title_list = df_my_rating['title'].tolist()
recommend_movie_list = similar_movie_list.loc[ ~similar_movie_list['title'].isin(title_list),]

recommend.py 전체 소스 코드

from flask import request
from flask_restful import Resource
from flask_jwt_extended import get_jwt_identity, jwt_required
import mysql.connector
from ref.mysql_connection import get_connection

import pandas as pd
import numpy as np

class MovieRecommendRealTimeResource(Resource) :
    @jwt_required()
    def get(self):
        user_id = get_jwt_identity()
        try :
            connection = get_connection()
            # 전체 영화 리스트를 DB에서 호출
            query = ''' select r.user_id, r.movie_id, r.rating, m.title from rating r
                        join movie m on r.movie_id = m.id; '''
            cursor = connection.cursor(dictionary=True)
            cursor.execute(query)
            movie_list = cursor.fetchall()

            # 유저의 별점 정보를 DB에서 호출
            query = '''select r.user_id, r.movie_id, r.rating, m.title
                    from rating r join movie m
                        on r.movie_id = m.id and r.user_id = %s;'''
            record = (user_id, )
            cursor = connection.cursor(dictionary = True)
            cursor.execute(query, record)
            result_list = cursor.fetchall()
            cursor.close()
            connection.close()

        except mysql.connector.Error as e :
            print(e)
            cursor.close()
            connection.close()
            return {"error" : str(e)}, 503 #HTTPStatus.SERVICE_UNAVAILABLE

        movie_rating_df = pd.DataFrame(data=movie_list)

        # 피벗 테이블로 상관계수 출력, 50개 이상의 별점평가
        corr_df = movie_rating_df.pivot_table(index='user_id', columns='title', values='rating', aggfunc='mean')
        df = corr_df.corr(min_periods=50)

        # 내 별점 정보 데이터프레임에 저장
        df_my_rating = pd.DataFrame(data=result_list)

        # 추천 영화 리스트 데이터프레임 초기화
        similar_movie_list = pd.DataFrame()

        for i in range(  len(df_my_rating)  ) :
            similar_movie = df[df_my_rating['title'][i]].dropna().sort_values(ascending=False).to_frame()
            similar_movie.columns = ['Correlation']
            similar_movie['Weight'] = df_my_rating['rating'][i] * similar_movie['Correlation']
            similar_movie_list = pd.concat([similar_movie_list, similar_movie])

        # 영화 제목이 중복된 영화가 있을 수 있다.
        # 중복된 영화는, Weight 가 가장 큰(max)값으로 해준다.
        similar_movie_list.reset_index(inplace=True)
        similar_movie_list = similar_movie_list.groupby('title')['Weight'].max().sort_values(ascending=False)

        # 내가 이미 봐서, 별점을 남긴 영화는 여기서 제외해야 한다.
        similar_movie_list = similar_movie_list.reset_index()

        # 내가 이미 본 영화 제목만 가져온다.
        title_list = df_my_rating['title'].tolist()

        # similar_movie_list 에 내가 본영화인 title_list 를
        # 제외하고 가져온다.
        recomm_movie_list = similar_movie_list.loc[ ~similar_movie_list['title'].isin(title_list) ,   ]

        
        return {'result' : 'success' ,
                "count" : len(recomm_movie_list.iloc[ 0 : 10 , ].to_dict('records')),
                'movie_list' : recomm_movie_list.iloc[ 0 : 10 , ].to_dict('records') }
반응형