<이전 코드/>
import psycopg2, os
import pandas as pd
import numpy as np
import datetime
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics.pairwise import cosine_similarity
# 특정 정보와 서비스명 유사도가 높은 서비스 정보를 얻기 위한 함수
def recommendation(id, top_n=10):
# 데이터베이스 연결
con = psycopg2.connect(
host=os.environ.get("DB_HOST"),
dbname=os.environ.get("DB_NAME"),
user=os.environ.get("DB_USER"),
password=os.environ.get("DB_PASSWORD"),
port=os.environ.get("DB_PORT"),
)
cur = con.cursor()
cur.execute(
"SELECT id, info_name, location, category, start_date, end_date, svstatus From exhibitions_exhibition"
)
cols = [column[0] for column in cur.description]
exhibition_df = pd.DataFrame.from_records(data=cur.fetchall(), columns=cols)
con.close() # 데이터베이스 연결 종료
# info_name별 유사도 측정
count_vect = CountVectorizer(min_df=0, ngram_range=(1, 2))
info_name_mat = count_vect.fit_transform(exhibition_df["info_name"])
# 유사도 행렬 생성
info_name_sim = cosine_similarity(info_name_mat, info_name_mat)
# 입력한 정보의 index
target_info_name = exhibition_df[exhibition_df["id"] == id]
target_index = target_info_name.index.values
# 입력한 정보의 유사도 데이터 프레임 추가
exhibition_df["similarity"] = info_name_sim[target_index, :].reshape(-1, 1)
# start_date가 오늘 날짜 이전이거나 오늘이고, end_date가 오늘 날짜거나 오늘 날짜 이후인 데이터만 추출하기
today = datetime.date.today()
period = (exhibition_df["start_date"] <= today) & (
exhibition_df["end_date"] >= today
)
filterd_exhibition_df = exhibition_df.loc[period].sort_values(
by=["end_date"], ascending=True
)
# 필터링 된 데이터프레임의 유사도 내림차순 정렬 후 상위 index 추출
temp = filterd_exhibition_df.sort_values(by=["similarity"], ascending=False)
temp = temp[temp.index.values != target_index] # 자기 자신 제거
temp = temp[temp["svstatus"] == "접수중"] # svstatus가 접수중인 데이터만 추출하기
final_index = temp.index.values[:top_n]
raw_exhibitions = filterd_exhibition_df.loc[final_index]
ml_recommend_exhibitions_id_list = list(
np.array(raw_exhibitions[["id"]]["id"].tolist())
)
return ml_recommend_exhibitions_id_list
<문제점/>
- 추천 함수가 전시 데이터를 상세 조회할 때 실행되는데 프론트엔드에서 상세 페이지에 접속할 때마다 데이터베이스에 접속해서 데이터를 받아온다
- 원래는 서버 실행 시 한 번 데이터베이스에 접속했었는데 새로운 전시 데이터가 추가 됐을 때 서버 실행할 때 처음 받아온 데이터에 새로운 데이터 정보가 없어서 에러가 발생한다
- 그래서 데이터베이스에 접속하는 로직을 추천 함수 안에 넣었는데 아무리 생각해도 매번 데이터베이스에 접속하는 건 비효율적인 것 같아 리팩토링을 시도했다
<수정한 코드/>
- pre_processing 함수 생성 후 실행
def pre_processing():
# 데이터베이스 연결
con = psycopg2.connect(
host=os.environ.get("DB_HOST"),
dbname=os.environ.get("DB_NAME"),
user=os.environ.get("DB_USER"),
password=os.environ.get("DB_PASSWORD"),
port=os.environ.get("DB_PORT"),
)
cur = con.cursor()
cur.execute(
"SELECT id, info_name, start_date, end_date, svstatus From exhibitions_exhibition"
)
cols = [column[0] for column in cur.description]
# 데이터프레임 생성
exhibition_df = pd.DataFrame.from_records(data=cur.fetchall(), columns=cols)
# 데이터베이스 연결 종료
con.close()
# info_name별 유사도 측정
count_vect = CountVectorizer(min_df=0, ngram_range=(1, 2))
info_name_mat = count_vect.fit_transform(exhibition_df["info_name"])
# 유사도 행렬 생성
info_name_sim = cosine_similarity(info_name_mat, info_name_mat)
return exhibition_df, info_name_sim
exhibition_df, info_name_sim = pre_processing()
- 데이터베이스 접속 후 유사도 행렬 생성하는 부분까지 일련의 전처리 과정을 pre_processing 함수에 담았다
- 이전 코드에서는 전역에 있었는데 전시 데이터가 추가됐을 때 데이터베이스에 접속하는 과정을 다시 실행해야 하기 때문에 함수에 담았다
- 전시 데이터프레임과 유사도 행렬을 반환해서 전역에서 exhibition_df랑 info_name_sim 변수를 선언해 pre_processing 함수를 실행해 반환된 값을 담는다
- recommendation 함수 수정
# 비슷한 전시 추천 시스템 함수
def recommendation(id, top_n=5):
# 전역변수를 참조하도록 지정함
global exhibition_df
global info_name_sim
try:
# 입력한 정보의 index
target = exhibition_df[exhibition_df["id"] == id]
target_index = target.index.values
# 데이터프레임에 입력한 정보의 유사도 추가
exhibition_df["similarity"] = info_name_sim[target_index, :].reshape(-1, 1)
# start_date가 오늘 날짜 이전이거나 오늘이고, end_date가 오늘 날짜거나 오늘 날짜 이후인 데이터만 추출하기
today = datetime.date.today()
period = (exhibition_df["start_date"] <= today) & (
exhibition_df["end_date"] >= today
)
filterd_exhibition_df = exhibition_df.loc[period].sort_values(
by=["end_date"], ascending=True
)
# 날짜 필터링 된 데이터프레임의 유사도 내림차순 정렬
temp = filterd_exhibition_df.sort_values(by=["similarity"], ascending=False)
# 자기 자신 제거
temp = temp[temp.index.values != target_index]
# svstatus가 접수중인 데이터만 추출
temp = temp[temp["svstatus"] == "접수중"]
# 데이터프레임 기준 최종 유사도 Top5 index 리스트
final_index = temp.index.values[:top_n]
except ValueError:
exhibition_df, info_name_sim = pre_processing()
return recommendation(id, top_n)
# 최종 유사도 Top5의 데이터프레임
raw_exhibitions = filterd_exhibition_df.loc[final_index]
# 최종 유사도 Top5의 데이터베이스 상 id 리스트
ml_recommend_exhibitions_id_list = np.array(raw_exhibitions[["id"]]["id"].tolist())
return ml_recommend_exhibitions_id_list
- 전역에서 선언한 exhibition_df와 info_name_sim 변수를 recommendation 함수에서 사용하기 위해 global을 써서 일종의 import?를 해줬다
- 그리고 기존에 있었던 인자로 들어온 전시 데이터와 비교해서 그 데이터와 유사도가 높은 순대로 5개 전시 데이터를 추출하는 로직을 try문에 넣고 ValueError가 뜨면(새로운 전시가 추가된 후 그 전시 데이터를 조회하는 경우) except문에서 pre_processing 함수를 다시 실행한 후 exhibition_df와 info_name_sim에 새로운 데이터를 받아서 recommendation 함수를 실행한다
- 그리고 소소하지만 원래 ml_recommend_exhibitions_id_list 변수에 값을 담는 부분이 아래 코드였는데
ml_recommend_exhibitions_id_list = list(
np.array(raw_exhibitions[["id"]]["id"].tolist())
)
- 코드를 보니까 numpy 메서드로 이미 tolist를 해서 리스트 타입으로 변환된 것 같은데 바깥에서 또 list로 감싸주고 있어서 '과거의 나 ... 왜 그랬지...?' 하며 겉에 list()를 제거했다
- 실행 시간 비교
<추가 리팩토링/>
- try-except문 대신 전역 변수를 활용하여 코드의 가독성을 높이고, 에러 처리를 명확히 함.
- https://guco.tistory.com/304