세부 과제
- README.md - 프로젝트의 목적과 진행과정에 대해 작성한다.
- 크롤링을 통한 데이터 수집
- 재사용이 가능하도록 함수화
- 원본 데이터 파일 저장
- 데이터 전처리
- 필요없는 문자 제거
- 방법 1. 맞춤법 검사 / 띄어쓰기 교정
- 방법 2. 불용어 제거
- 방법 3. 정규표현식을 통한 특수문자 및 자음/모음 제거
- 리뷰의 길이 분포 확인
- 통계요약표 또는 히스토그램으로 확인 → 각 전처리의 선택 과정을 표현해주세요.
전처리 과정의 필요성
Tokenizing(토큰나이징) : 텍스트를 어떤 단어로 쪼개는 과정 Imbedding(임베딩) : tokenizing된 단어들을 숫자 벡터로 만드는 과정 ⇒ 쪼개진 단어가 곧 하나의 숫자로 변하게 되는 것
1.
필요없는 문자 제거
텍스트 분석은 문장에서 단어들의 배치 패턴을 파악하여 문맥을 이해
- ex) 문법에서 주어 다음 동사가 온다는 것을 학습을 통해 이해,
- 새로운 주어 다음에 동사를 예측하는 것
패턴을 잘 파악하기 위해서는 의미가 있는 단어들의 모음으로 벡터가 제작되어야함
행렬은 크기가 클수록 연산처리가 많아지고, 속도도 느려짐
리뷰의 길이 분포 확인
- 분석을 하기 위해서는 행렬 연산이 되어야 하기 때문에 리뷰의 길이가 다르더라도 임베딩되는 숫자 벡터의 길이는 동일하게 만듦
- 길이가 짧은 리뷰의 경우에는 임베딩할 길이를 맞추기 위해 0으로 채움
- 희소행렬(sparse matrix)는 메모리, 속도에 비해 비효율적이다. → 길이가 너무 짧거나 긴 문장을 제외하는 이유
- 자신이 가지고 있는 데이터를 보고, 어떤 방법이 데이터 분석을 하는데 유용할지 판단
- 과정에 이유가 있어야 다른 사람들과의 차별점이 된다.
큰 틀의 과제
- 데이터 수집
- 데이터 전처리
- 데이터 분석
1. 데이터 수집
-
서울 신라호텔 더 파크뷰에 대한 네이버 리뷰
-
리뷰를 얼마나 추출해야할까? - 200개에서 300개정도는 해야 어느 정도 윤곽이 잡히는 키워드들과 리뷰들에 대한 빅데이터를 가질 수 있다고 예상함
-
네이버 리뷰 링크를 주소로 사용하여 크롤링 - 추출할 대상: 아이디, 리뷰 텍스트, 방문 날짜, 몇번째 방문
-
리뷰 텍스트가 전부 보이지 않는 경우가 있다. - 그 경우, 내용 더보기 버튼을 클릭하여 텍스트가 전부 다 보이게 한다. - for문을 돌려서 텍스트들을 추출할 때 try/except문을 써서 내용 더보기 버튼이 없는 리뷰에 예외처리를 해줘야한다.
-
페이지를 내렸을 때, 더보기 버튼을 눌러야 다른 리뷰 확인가능
STEP 1. 순차적 개발
# Setting
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.actions.mouse_button import MouseButton
from selenium.webdriver import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import pandas as pd
url = "https://pcmap.place.naver.com/restaurant/13166754/review/visitor"# 크롬창 열기
options = Options()
driver = webdriver.Chrome(options = options)
# 창모드 전체화면으로 크기 늘리기
driver.maximize_window()
# 로딩시간: 페이지 로딩
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_all_elements_located((By.TAG_NAME, 'body')))
# 네이버 리뷰 주소 접속하기
driver.get(url)
# 로딩시간: 페이지 로딩
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_all_elements_located((By.TAG_NAME, 'body')))# 데이터 받을 빈 딕셔너리
review_data = {"UserID":[],"Review_Text":[],"Date":[], "NumberOfVisit":[]}
# 리뷰 200개까지 나오게 미리 더보기 클릭하기
# for문 19번 돌리는 이유는 처음에 10개 있는 것을 빼고 세야함
print("총 리뷰 개수 구하기")
for num_reviews in range(19):
driver.find_element(By.CLASS_NAME, 'fvwqf').click()
time.sleep(1)
rows = driver.find_elements(By.CLASS_NAME, 'owAeM')
# rows의 개수를 세어 for문을 돌린다.
cnt = len(rows)
print("총 리뷰 개수: ", cnt)
# 리뷰 추출하기
log_count = 0
print("START", end = " >>> ")
for i in range(cnt):
# 리뷰 내용 더보기 버튼이 없는 리뷰들 예외구문
try:
review_text_extend = rows[i].find_element(By.CLASS_NAME, 'Ky28p').click()
except:
pass
user_id = rows[i].find_element(By.CLASS_NAME, 'qgLL3')
review_text = rows[i].find_element(By.CLASS_NAME, 'zPfVt')
number_visits = rows[i].find_elements(By.CLASS_NAME, 'CKUdu')[1]
visit_date = rows[i].find_elements(By.CLASS_NAME, 'CKUdu')[0].find_elements(By.CLASS_NAME, 'place_blind')[1]
review_data['UserID'].append(user_id.text)
review_data['Review_Text'].append(review_text.text)
review_data['Date'].append(visit_date.text)
review_data['NumberOfVisit'].append(number_visits.text)
# 로그 만들기
log_count += 1
if log_count > 0 and log_count % 20 == 0:
print(log_count, end= " >>> ")
print("End")
print(review_data)# 데이터프레임 만들기
df_reviews = pd.DataFrame(review_data)
# .csv파일로 저장
df_reviews.to_csv("./shilla_hotel_buffet_review_original.csv")STEP 2. 계획 설정
- 전체 함수 이름 결정:
get_review(url) - 덩어리별로 분류:
- 크롬 창 열고 url 접속하기:
open_url(url), output:driver - 더보기 버튼을 눌러 총 리뷰 갯수 구하기:
count_reviews(driver), output:rows, cnt - 리뷰 데이터를 추출하기:
crawling_reviews(rows, cnt), output:review_data
- 크롬 창 열고 url 접속하기:
STEP 3. 최종 코드 정리
# Function 정리
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.actions.mouse_button import MouseButton
from selenium.webdriver import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import pandas as pd# 이번에 쓰이는 Functions
def open_url(url):
"""크롬 창을 열고 url에 접속하는 함수"""
# 크롬창 열기
options = Options()
driver = webdriver.Chrome(options = options)
# 창모드 전체화면으로 크기 늘리기
driver.maximize_window()
# 로딩시간: 페이지 로딩
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_all_elements_located((By.TAG_NAME, 'body')))
# 네이버 리뷰 주소 접속하기
driver.get(url)
# 로딩시간: 페이지 로딩
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_all_elements_located((By.TAG_NAME, 'body')))
return driver
def count_reviews(driver):
"""더보기 버튼을 통해 구하고자 하는 리뷰 갯수의 총 갯수를 구하는 함수"""
for num_reviews in range(19):
driver.find_element(By.CLASS_NAME, 'fvwqf').click()
time.sleep(1)
rows = driver.find_elements(By.CLASS_NAME, 'owAeM')
# rows의 개수를 세어 for문을 돌린다.
cnt = len(rows)
print("총 리뷰 개수:", cnt)
return rows, cnt
def crawling_reviews(rows, cnt):
"""리뷰 데이터를 추출하는 함수"""
# 데이터 받을 빈 딕셔너리
review_data = {"UserID":[],"Review_Text":[],"Date":[], "NumberOfVisit":[]}
# 리뷰 추출하기
log_count = 0
print("START", end = " >>> ")
for i in range(cnt):
# 리뷰 내용 더보기 버튼이 없는 리뷰들 예외구문
try:
rows[i].find_element(By.CLASS_NAME, 'Ky28p').click()
except:
pass
user_id = rows[i].find_element(By.CLASS_NAME, 'qgLL3')
review_text = rows[i].find_element(By.CLASS_NAME, 'zPfVt')
number_visits = rows[i].find_elements(By.CLASS_NAME, 'CKUdu')[1]
visit_date = rows[i].find_elements(By.CLASS_NAME, 'CKUdu')[0].find_elements(By.CLASS_NAME, 'place_blind')[1]
review_data['UserID'].append(user_id.text)
review_data['Review_Text'].append(review_text.text)
review_data['Date'].append(visit_date.text)
review_data['NumberOfVisit'].append(number_visits.text)
# 로그 만들기
log_count += 1
if log_count > 0 and log_count % 20 == 0:
print(log_count, end= " >>> ")
print("End")
return review_data# 메인 함수
def get_review(url):
"""메인 함수"""
# 크롬 창 열기
print(f"크롬 창을 열고 있습니다.", end=" ")
driver = open_url(url)
print()
title = driver.find_element(By.CLASS_NAME, 'GHAhO').text
# 총 리뷰 개수 구하기
print("'{0}'의 리뷰를 가지고 오는 중입니다.".format(title))
rows, cnt = count_reviews(driver)
# 딕셔너리 만들기
review_data = crawling_reviews(rows,cnt)
# 데이터프레임 만들기
print("데이터프레임으로 변환 중입니다.",end=" ")
df_reviews = pd.DataFrame(review_data)
print("... 완료!")
# .csv파일로 저장
df_reviews.to_csv("./shilla_hotel_buffet_review_original.csv")
driver.quit()
return df_reviews2. 데이터 전처리
Reference
동국대 텍스트 전처리
한국어 불용어 처리 1
한국어 불용어 처리 2
위키독스
한국어 텍스트 전처리
Github정리
이모티콘 제거
- 데이터전처리 방법은 작성자 마음이다.
데이터 전처리시 고려해야할 사항
- 특수문자, 초성기호 등 제거
re.sub()함수 쓰기
- 문장의 길이
- 문장의 길이 이상치를 정한 다음 이를 넘기거나 혹은 이에 준하지 못하는 문장은 제거
- 맞춤법 / 띄어쓰기
pyhanspell/pykospacing사용- 시간이 오래 걸린다는 단점
정규 표현식을 이용한 문자 제거
맞춤법 / 띄어쓰기
최종 코드 정리
- 특수문자등을 처리
# Setting
import pandas as pd
import re
def text_preprocessing(df_reviews):
"""Review_Text의 리뷰글을 전처리 하는 함수"""
for i in range(len(df_reviews)):
text = df_reviews['Review_Text'][i]
if not isinstance(text, str):
text = str(text)
# 한글, 영어, 숫자, 공백 문자를 제외한 모든 문자를 제거
temp = re.sub('[^가-힣a-zA-Z0-9\s]', '',text)
df_reviews.loc[[i],['Review_Text']] = temp
# 열에 있는 개행 문자 제거
df_reviews['Review_Text'] = df_reviews['Review_Text'].str.replace('\n','')
# 열 제거
df_reviews = df_reviews.drop('Unnamed: 0', axis = 1)
df_reviews['Review_Text'] = df_reviews['Review_Text'].str.replace('nan', '')
df_reviews.to_csv('./S_hotel_buffet_review_data_preprocess.csv')
return df_reviews# 실행
shilla_hotel_buffet_review = pd.read_csv("C:/Users/pps/Desktop/Restaurant_Review/Data_Collect/shilla_hotel_buffet_review_original.csv", encoding='utf-8')
S_hotel_buffet_preprocess_review = text_preprocessing(shilla_hotel_buffet_review)- 맞춤법 확인
# Setting
from hanspell import spell_checker
requestURL = "https://m.search.naver.com/p/csearch/ocontent/util/SpellerProxy?passportKey=90ae3cbdae22968de3f12b9095f35ea488ecf40b&_callback=jQuery112406350187405022272_1718946457167&q=%EB%A7%9E%EC%B6%A4%EB%B2%95+%EA%B2%80%EC%82%AC%EB%A5%BC+%EC%9B%90%ED%95%98%EB%8A%94+%EB%8B%A8%EC%96%B4%EB%82%98+%EB%AC%B8%EC%9E%A5%EC%9D%84+%EC%9E%85%EB%A0%A5%ED%95%B4+%EC%A3%BC%EC%84%B8%EC%9A%94.&where=nexearch&color_blindness=0&_=1718946457168"
def ReviewSpellChecker(df_reviews):
"""Review_Text의 맞춤법 고치는 함수"""
for i in range(len(df_reviews)):
sentence = df_reviews['Review_Text'][i]
# 만약 리뷰가 없는 글이 있다면 처리
if len(sentence) > 0:
result = spell_checker.check(df_reviews['Review_Text'][i], requestURL).checked
else:
result = ""
df_reviews.loc[i, 'Review_Text'] = result
df_reviews = df_reviews.drop('Unnamed: 0', axis = 1)
df_reviews.to_csv('./S_hotel_buffet_review_spellchecker.csv')
return df_reviews# 실행
buffet_preprocess_review = pd.read_csv("C:/Users/pps/Desktop/Restaurant_Review/Data_Preprocessing/S_hotel_buffet_review_data_preprocess.csv")
buffet_spellchecker_review = ReviewSpellChecker(buffet_preprocess_review)- 띄어쓰기 확인
# Setting
from pykospacing import Spacing
spacing = Spacing()
def checkSpacing(df_reviews):
"""Review_Text의 띄어쓰기 오류 고치는 함수"""
for i in range(len(df_reviews)):
print(i, df_reviews["Review_Text"][i])
test_space = spacing(df_reviews["Review_Text"][i])
df_reviews.loc[i, 'Review_Text'] = test_space
df_reviews.to_csv('./S_hotel_buffet_review_spacingcheck.csv')
return df_reviews# 실행
spellchecker_review = pd.read_csv("C:/Users/pps/Desktop/Restaurant_Review/Data_Preprocessing/S_hotel_buffet_review_spellchecker.csv")
spacingcheck_review = checkSpacing(spellchecker_review)문장(리뷰)의 길이 확인
def find_reviewlength(df_reviews):
"""리뷰글의 길이를 추출하는 함수"""
review_strlength = {"리뷰 길이":[]}
for i in range(len(df_reviews)):
temp_length = len(df_reviews["Review_Text"][i])
review_strlength["리뷰 길이"].append(temp_length)
df_temp_review_strlength = pd.DataFrame(review_strlength)
return df_temp_review_strlength# 실행
spacingchecker_review = pd.read_csv("C:/Users/pps/Desktop/Restaurant_Review/Data_Preprocessing/S_hotel_buffet_review_spacingcheck.csv")
s_hotel_buffet_review_strlength = find_reviewlength(spacingchecker_review)이상치 확인 및 제거
def find_IQR(df_length_reviews,df_reviews):
"""IQR방식을 이용한 이상치를 찾아 제거하는 함수"""
new_df_length = df_length_reviews.copy()
new_df_reviews = df_reviews.copy()
Q1 = new_df_length["리뷰 길이"].quantile(q=0.25)
Q3 = new_df_length["리뷰 길이"].quantile(q=0.75)
IQR = Q3 - Q1
IQR_df = new_df_length[(new_df_length["리뷰 길이"] >= Q3 + 1.5 * IQR) | (new_df_length["리뷰 길이"] <= Q1 - 1.5*IQR)].index
new_df_reviews = new_df_reviews.drop('Unnamed: 0.1', axis = 1)
new_df_reviews = new_df_reviews.drop('Unnamed: 0', axis = 1)
new_df_reviews.drop(IQR_df, inplace = True)
new_df_reviews.reset_index(drop=True, inplace = True)
new_df_reviews.to_csv("./S_hotel_buffet_review_IQR.csv")
return new_df_reviews# 실행
s_hotel_buffet_review_outlier = find_IQR(s_hotel_buffet_review_strlength, spacingchecker_review)3. 데이터 분석
1. 형태소 분리
def df_to_string(df, column):
"""DataFrame을 String으로 변환하는 함수"""
df_list = df[column].to_list()
df_string = ''.join(str(s) for s in df_list)
return df_string1. 명사 분류하기 및 시각화
# Setting
import pandas as pd
from wordcloud import WordCloud
import matplotlib.pyplot as plt
from collections import Counter
from kiwipiepy import Kiwi
kiwi = Kiwi()
def kiwi_noun_extractor(text):
"""kiwipiepy패키지를 이용해서 명사를 분류하는 함수"""
results = []
result = kiwi.analyze(text)
for token, pos, _, _ in result[0][0]:
# kiwi의 태그 목록 체언:
# NNG: 일반 명사, NNP: 고유 명사, NNB: 의존 명사, NR: 수사, NP: 대명사
if len(token) != 1 and pos.startswith('N'): #or pos.startswith('SL'):
results.append(token)
return resultsdef wordcloud_noun(noun_text):
"""워드클라우드를 통해 시각화하는 함수"""
cnt = len(noun_text)
counts = Counter(noun_text)
tags_noun = counts.most_common(cnt)
wc = WordCloud(font_path='C:/Users/pps/AppData/Local/Microsoft/Windows/Fonts/NanumBarunGothic.ttf', background_color='white', width=800, height=600)
cloud_noun = wc.generate_from_frequencies(dict(tags_noun))
plt.figure(figsize = (10, 8))
plt.axis('off')
plt.imshow(cloud_noun)
plt.show()# 실행
s_hotel_buffet_review_outlier = pd.read_csv("C:/Users/pps/Desktop/Restaurant_Review/Data_Preprocessing/S_hotel_buffet_review_IQR.csv")
s_hotel_buffet_review_outlier_string = df_to_string(s_hotel_buffet_review_outlier, "Review_Text")
s_hotel_buffet_review_nouns = kiwi_noun_extractor(s_hotel_buffet_review_outlier_string)
wordcloud_noun(s_hotel_buffet_review_nouns)- 출력값

2. 동사/형용사 분류하기 및 시각화
# Setting
import pandas as pd
from wordcloud import WordCloud
import matplotlib.pyplot as plt
from collections import Counter
from kiwipiepy import Kiwi
kiwi = Kiwi()
def kiwi_verb_adj_extractor(text):
"""kiwipiepy패키지를 이용해서 형용사,동사를 분류하는 함수"""
results = []
result = kiwi.analyze(text)
for token,pos,_,_ in result[0][0]:
if len(token) != 1 and pos.startswith('VA') or pos.startswith('VV'):
results.append(token)
f_results = list(map(lambda x : x + '다',results))
return f_resultsdef wordcloud_verb_adj(verb_adj_text):
"""워드클라우드를 통해 시각화하는 함수"""
cnt = len(verb_adj_text)
counts = Counter(verb_adj_text)
tags_verb_adj = counts.most_common(cnt)
wc = WordCloud(font_path='C:/Users/pps/AppData/Local/Microsoft/Windows/Fonts/NanumBarunGothic.ttf', background_color='white', width=800, height=600)
cloud_verb_adj = wc.generate_from_frequencies(dict(tags_verb_adj))
plt.figure(figsize = (10, 8))
plt.axis('off')
plt.imshow(cloud_verb_adj)
plt.show()# 실행
s_hotel_buffet_review_outlier = pd.read_csv("C:/Users/pps/Desktop/Restaurant_Review/Data_Preprocessing/S_hotel_buffet_review_IQR.csv")
s_hotel_buffet_review_outlier_string = df_to_string(s_hotel_buffet_review_outlier, "Review_Text")
s_hotel_buffet_review_verb_adj = kiwi_verb_adj_extractor(s_hotel_buffet_review_outlier_string)
wordcloud_verb_adj(s_hotel_buffet_review_verb_adj)- 출력값

2. 만족도,맛,서비스,가격으로 카테고리 나눠 긍정/부정 분류
# Setting
import os
import pandas as pd
import json
from dotenv import load_dotenv
from langchain_openai import ChatOpenAIclass MyChain:
"""chain을 만들어 프롬프트와 연결하는 클래스"""
def __init__(self, template):
self.llm = ChatOpenAI()
self.prompt = PromptTemplate.from_template(template)
def invoke(self, review_text):
input_data = {"sentence": review_text}
result = (self.prompt | self.llm).invoke(input_data)
return result
def parsing(output):
"""분류된 json형식을 딕셔너리로 바꾸는 함수"""
try:
result_dict = json.loads(output)
except json.JSONDecodeError:
result_dict = {}
return result_dict
def save_parse_reviews(df, chain):
"""분류된 데이터들을 데이터프레임에 저장하는 함수"""
temp = {"만족도": [], "맛":[], "서비스":[], "가격":[]}
for sentence in df["Review_Text"]:
emo_eval = chain.invoke(sentence)
test_result = parsing(emo_eval.content)
temp["만족도"].append(test_result["만족도"])
temp["맛"].append(test_result["맛"])
temp["서비스"].append(test_result["서비스"])
temp["가격"].append(test_result["가격"])
df["만족도"] = temp["만족도"]
df["맛"] = temp["맛"]
df["서비스"] = temp["서비스"]
df["가격"] = temp["가격"]
df.to_csv("./S_hotel_buffet_review_parse.csv")
return df
load_dotenv()
template = """\
# INSTRUCTION
- 당신은 긍/부정 분류기입니다.
- 각 대상 '만족도', '맛', '서비스', '가격'에 대한 평가가 긍정적인지 부정적인지를 분류하세요.
- 대상에 대한 평가가 없는 경우 '-'을 표시하세요.
- 예시를 보고 결과를 다음과 같은 딕셔너리 형식으로 출력하세요:
"만족도": "긍정/부정/-",
"맛": "긍정/부정/-",
"서비스": "긍정/부정/-",
"가격": "긍정/부정/-"
# SENTENCE: {sentence}
"""# 실행
s_hotel_buffet_review_outlier = pd.read_csv("C:/Users/pps/Desktop/Restaurant_Review/Data_Preprocessing/S_hotel_buffet_review_IQR.csv")
s_hotel_buffet_review_outlier = s_hotel_buffet_review_outlier.drop('Unnamed: 0', axis = 1)
chain = MyChain(template=template)
s_hotel_buffet_parse_review = save_parse_reviews(s_hotel_buffet_review_outlier, chain)
s_hotel_buffet_parse_review.head(10)- 출력값

3. 긍/부정으로 분류된 데이터들을 시각화하여 분석하기/나타내기
1. 사람들이 리뷰글로 얼마나 많은 긍/부정 평가를 했는지 막대그래프를 사용하여 시각화
import pandas as pd
review = pd.read_csv("C:/Users/pps/Desktop/Restaurant_Review/Data_Analyze/S_hotel_buffet_review_parse.csv")
temp_review = review.copy()각 카테고리별 긍/부정 개수 구하기
# Setting
def emotion_eval_counts(df, column_name):
return df[column_name].value_counts()
# 실행
print(emotion_eval_counts(review, "만족도"))
print(emotion_eval_counts(review, "맛"))
print(emotion_eval_counts(review, "서비스"))
print(emotion_eval_counts(review, "가격"))- 출력값

막대그래프 시각화
# 한글 글꼴 설정
import matplotlib.pyplot as plt
import numpy as np
font_path = 'C:/Users/pps/AppData/Local/Microsoft/Windows/Fonts/NanumBarunGothic.ttf'
font_name = plt.matplotlib.font_manager.FontProperties(fname=font_path).get_name()
plt.rcParams['font.family'] = font_namedef s_hotel_review_emote_bar(emote1,emote2,emote3,emote4):
"""카테고리 4개인 막대그래프 만드는 함수"""
x = np.arange(4)
satisf_Aemo = emote1["긍정"] + emote1["부정"]
taste_Aemo = emote2["긍정"] + emote2["부정"]
service_Aemo = emote3["긍정"] + emote3["부정"]
price_Aemo = emote4["긍정"] + emote4["부정"]
y_axis = [satisf_Aemo,taste_Aemo,service_Aemo,price_Aemo]
x_axis = ["만족도","맛","서비스","가격"]
plt.bar(x,y_axis)
plt.xticks(x, x_axis)
plt.title("카테고리별 긍/부정 갯수")
plt.show()# 실행
satisfy_emote = emotion_eval_counts(review, "만족도")
taste_emote = emotion_eval_counts(review, "맛")
service_emote = emotion_eval_counts(review, "서비스")
price_emote = emotion_eval_counts(review, "가격")
s_hotel_review_emote_bar(satisfy_emote,taste_emote,service_emote,price_emote)- 출력값

2. 긍정 평가가 많은 카테고리, 부정 평가가 많은 카테고리 확인
- 위에서 구한 각 카테고리별 긍정/부정 갯수를 바탕으로 결과 도출
- 긍정 평가가 많은 카테고리 =
만족도 - 부정 평가가 많은 카테고리 =
가격
- 긍정 평가가 많은 카테고리 =
3. 긍/부정평가가 가장 많은 카테고리의 각 긍/부정 리뷰글만 추출하여 데이터 시각화
1. 만족도,가격의 긍/부정 리뷰글들의 명사 추출, 동사/형용사 추출및 시각화
만족도 긍정 리뷰글
- 긍정 리뷰 분류
# Setting
def positive_review_extractor(df, string_column, category_column):
"""
카테고리에 긍정으로 표시된 리뷰글만 추출하는 함수
Args:
df (_type_): 데이터프레임
string_column (_type_): 리뷰글항목
category_column (_type_): 카테고리명
"""
positive_reviews = {"긍정":[]}
for i in range(len(df)):
if df[category_column][i] == "긍정":
positive_reviews["긍정"].append(df[string_column][i])
return positive_reviews# 실행
satisfy_review = positive_review_extractor(review, "Review_Text","만족도")- 문자열 추출
# Setting
def positive_review_string_extractor(df):
"""딕셔너리로 받은 리뷰글 추출하는 함수"""
for key, value in df.items():
df[key] = ', '.join(value)
positive_string = df.get("긍정")
return positive_string# 실행
satisfy_positive_string = positive_review_string_extractor(satisfy_review)- 명사 추출
# Setting
from kiwipiepy import Kiwi
kiwi = Kiwi()
def kiwi_noun_extractor(text):
"""명사 추출하는 함수"""
results = []
result = kiwi.analyze(text)
for token, pos, _, _ in result[0][0]:
if len(token) != 1 and pos.startswith('N'): #or pos.startswith('SL'):
results.append(token)
return results# 실행
satisfy_reviews_noun = kiwi_noun_extractor(satisfy_positive_string)- 동사/형용사 추출
# Setting
from kiwipiepy import Kiwi
kiwi = Kiwi()
def kiwi_verb_adj_extractor(text):
"""동사/형용사 추출하는 함수"""
results = []
result = kiwi.analyze(text)
for token,pos,_,_ in result[0][0]:
if len(token) != 1 and pos.startswith('VA') or pos.startswith('VV'):
results.append(token)
f_results = list(map(lambda x : x + '다',results))
return f_results# 실행
satisfy_reviews_verb_adj = kiwi_verb_adj_extractor(satisfy_positive_string)- 명사 시각화
# Setting
from wordcloud import WordCloud
import matplotlib.pyplot as plt
from collections import Counter
def wordcloud_noun(noun_text):
"""명사만 워드클라우드로 나타내는 함수"""
cnt = len(noun_text)
counts = Counter(noun_text)
tags_noun = counts.most_common(cnt)
wc = WordCloud(font_path='C:/Users/pps/AppData/Local/Microsoft/Windows/Fonts/NanumBarunGothic.ttf', background_color='white', width=800, height=600)
cloud_noun = wc.generate_from_frequencies(dict(tags_noun))
plt.figure(figsize = (10, 8))
plt.axis('off')
plt.imshow(cloud_noun)
title_font = {
'fontsize':16,
'fontweight': 'bold'
}
plt.title("만족도 긍정 리뷰글의 명사모음", fontdict=title_font)
plt.show()# 실행
wordcloud_noun(satisfy_reviews_noun)- 동사/형용사 시각화
# 워드클라우드 시각화
from wordcloud import WordCloud
import matplotlib.pyplot as plt
from collections import Counter
def wordcloud_verb_adj(verb_adj_text):
"""동사/형용사를 워드클라우드로 나타내는 함수"""
cnt = len(verb_adj_text)
counts = Counter(verb_adj_text)
tags_verb_adj = counts.most_common(cnt)
wc = WordCloud(font_path='C:/Users/pps/AppData/Local/Microsoft/Windows/Fonts/NanumBarunGothic.ttf', background_color='white', width=800, height=600)
cloud_verb_adj = wc.generate_from_frequencies(dict(tags_verb_adj))
plt.figure(figsize = (10, 8))
plt.axis('off')
plt.imshow(cloud_verb_adj)
title_font = {
'fontsize':16,
'fontweight': 'bold'
}
plt.title("만족도 긍정 리뷰글의 동사/형용사 모음", fontdict=title_font)
plt.show()# 실행
wordcloud_verb_adj(satisfy_reviews_verb_adj)- 출력값


가격 부정 리뷰글
- 부정 리뷰 분류
# 실행
def negative_review_extractor(df, string_column, category_column):
"""
카테고리에 부정으로 표시된 리뷰글만 추출하는 함수
Args:
df (_type_): 데이터프레임
string_column (_type_): 리뷰글항목
category_column (_type_): 카테고리명
"""
negative_reviews = {"부정":[]}
for i in range(len(df)):
if df[category_column][i] == "부정":
negative_reviews["부정"].append(df[string_column][i])
return negative_reviews# 실행
price_reviews = negative_review_extractor(review, "Review_Text","가격")# Setting
def negative_review_string_extractor(df):
"""딕셔너리로 받은 리뷰글 추출하는 함수"""
for key, value in df.items():
df[key] = ', '.join(value)
negative_string = df.get("부정")
return negative_string# 실행
price_negative_string = negative_review_string_extractor(price_reviews)- 명사 추출
# 실행
price_reviews_noun = kiwi_noun_extractor(price_negative_string)- 동사/형용사 추출
price_reviews_verb_adj = kiwi_verb_adj_extractor(price_negative_string)- 명사 시각화
# 워드클라우드 시각화
from wordcloud import WordCloud
import matplotlib.pyplot as plt
from collections import Counter
def wordcloud_noun(noun_text):
"""명사만 워드클라우드로 나타내는 함수"""
cnt = len(noun_text)
counts = Counter(noun_text)
tags_noun = counts.most_common(cnt)
wc = WordCloud(font_path='C:/Users/pps/AppData/Local/Microsoft/Windows/Fonts/NanumBarunGothic.ttf', background_color='white', width=800, height=600)
cloud_noun = wc.generate_from_frequencies(dict(tags_noun))
plt.figure(figsize = (10, 8))
plt.axis('off')
plt.imshow(cloud_noun)
title_font = {
'fontsize':16,
'fontweight': 'bold'
}
plt.title("가격 부정 리뷰글의 명사모음", fontdict=title_font)
plt.show()# 실행
wordcloud_noun(price_reviews_noun)- 동사/형용사 시각화
# 워드클라우드 시각화
from wordcloud import WordCloud
import matplotlib.pyplot as plt
from collections import Counter
def wordcloud_verb_adj(verb_adj_text):
"""동사/형용사를 워드클라우드로 나타내는 함수"""
cnt = len(verb_adj_text)
counts = Counter(verb_adj_text)
tags_verb_adj = counts.most_common(cnt)
wc = WordCloud(font_path='C:/Users/pps/AppData/Local/Microsoft/Windows/Fonts/NanumBarunGothic.ttf', background_color='white', width=800, height=600)
cloud_verb_adj = wc.generate_from_frequencies(dict(tags_verb_adj))
plt.figure(figsize = (10, 8))
plt.axis('off')
plt.imshow(cloud_verb_adj)
title_font = {
'fontsize':16,
'fontweight': 'bold'
}
plt.title("가격 부정 리뷰글의 동사/형용사 모음", fontdict=title_font)
plt.show()# 실행
wordcloud_verb_adj(price_reviews_verb_adj)- 출력값


4. 각 리뷰글의 언급된 빈도 수가 큰 명사 3개 추출및 긍정/부정 분류
만족도 긍정 리뷰글 추출
import pandas as pd
review = pd.read_csv("C:/Users/pps/Desktop/Restaurant_Review/Data_Analyze/S_hotel_buffet_review_parse.csv")
temp_review = review.copy()satisfy_reviews = positive_review_extractor(review, "Review_Text", "만족도")
satisfy_review_df = pd.DataFrame(satisfy_reviews)만족도 긍정 리뷰글 빈도 높은 명사 긍정/ 부정 분류
# Setting
import os
import pandas as pd
import json
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
class MyChain:
"""chain을 만들어 프롬프트와 연결하는 클래스"""
def __init__(self, template):
self.llm = ChatOpenAI()
self.prompt = PromptTemplate.from_template(template)
def invoke(self, review_text):
input_data = {"sentence": review_text}
result = (self.prompt | self.llm).invoke(input_data)
return result
def parsing(output):
"""분류된 json형식을 딕셔너리로 바꾸는 함수"""
try:
result_dict = json.loads(output)
except json.JSONDecodeError:
result_dict = {}
return result_dict
def save_satisfy_positive_reviews(df, chain):
"""분류된 데이터들을 데이터프레임에 저장하는 함수"""
temp = {"디저트": [], "음식":[], "친절":[]}
for sentence in df["긍정"]:
emo_eval = chain.invoke(sentence)
test_result = parsing(emo_eval.content)
temp["디저트"].append(test_result["디저트"])
temp["음식"].append(test_result["음식"])
temp["친절"].append(test_result["친절"])
df["디저트"] = temp["디저트"]
df["음식"] = temp["음식"]
df["친절"] = temp["친절"]
df.to_csv("./satisfy_review_nouns_emotion_classify.csv", index = False)
return df
load_dotenv()
template = """\
# INSTRUCTION
- 당신은 긍/부정 분류기입니다.
- 각 대상 '디저트', '음식', '친절'에 대한 평가가 긍정적인지 부정적인지를 분류하세요.
- 대상에 대한 평가가 없는 경우 '-'을 표시하세요.
- 예시를 보고 결과를 다음과 같은 딕셔너리 형식으로 출력하세요:
"디저트": "긍정/부정/-",
"음식": "긍정/부정/-",
"친절": "긍정/부정/-",
# SENTENCE:{sentence}
"""# 실행
chain = MyChain(template=template)
satisfy_positive_parse_review = save_satisfy_positive_reviews(satisfy_review_df, chain)가격 부정 리뷰글 추출
price_reviews = negative_review_extractor(review, "Review_Text","가격")
price_review_df = pd.DataFrame(price_reviews)가격 부정 리뷰글 빈도 높은 명사 긍정/부정 분류
# Setting
import os
import pandas as pd
import json
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
class MyChain:
"""chain을 만들어 프롬프트와 연결하는 클래스"""
def __init__(self, template):
self.llm = ChatOpenAI()
self.prompt = PromptTemplate.from_template(template)
def invoke(self, review_text):
input_data = {"sentence": review_text}
result = (self.prompt | self.llm).invoke(input_data)
return result
def parsing(output):
"""분류된 json형식을 딕셔너리로 바꾸는 함수"""
try:
result_dict = json.loads(output)
except json.JSONDecodeError:
result_dict = {}
return result_dict
def save_price_negative_reviews(df, chain):
"""분류된 데이터들을 데이터프레임에 저장하는 함수"""
temp = {"뷔페": [], "음식":[], "디저트":[]}
for sentence in df["부정"]:
emo_eval = chain.invoke(sentence)
test_result = parsing(emo_eval.content)
temp["뷔페"].append(test_result["뷔페"])
temp["음식"].append(test_result["음식"])
temp["디저트"].append(test_result["디저트"])
df["뷔페"] = temp["뷔페"]
df["음식"] = temp["음식"]
df["디저트"] = temp["디저트"]
df.to_csv("./price_review_nouns_emotion_classify.csv", index = False)
return df
load_dotenv()
template = """\
# INSTRUCTION
- 당신은 긍/부정 분류기입니다.
- 각 대상 '뷔페', '음식', '디저트'에 대한 평가가 긍정적인지 부정적인지를 분류하세요.
- 대상에 대한 평가가 없는 경우 '-'을 표시하세요.
- 예시를 보고 결과를 다음과 같은 딕셔너리 형식으로 출력하세요:
"뷔페": "긍정/부정/-",
"음식": "긍정/부정/-",
"디저트": "긍정/부정/-",
# SENTENCE:{sentence}
"""# 실행
chain = MyChain(template=template)
price_negative_parse_review = save_price_negative_reviews(price_review_df, chain)5. 빈도 높은 명사들의 총 긍정/부정 갯수와 비율 시각화
빈도 높은 명사들의 총 긍/부정 갯수 시각화
# Setting
def noun_emote_bar(emote1,emote2,emote3, column_name1, column_name2, column_name3, keyword):
"""명사 3개인 막대그래프 만드는 함수"""
x = np.arange(3)
all_emo1 = emote1["긍정"] + emote1["부정"]
all_emo2 = emote2["긍정"] + emote2["부정"]
all_emo3 = emote3["긍정"] + emote3["부정"]
y_axis = [all_emo1,all_emo2,all_emo3]
x_axis = [column_name1,column_name2,column_name3]
plt.bar(x,y_axis)
plt.xticks(x, x_axis)
plt.title("{} 긍/부정 갯수".format(keyword))
plt.show()# 만족도 긍정 리뷰글 빈도 높은 명사 갯수
dessert_pos_emo = emotion_eval_counts(satisfy_positive_parse_review, "디저트")
food_pos_emo = emotion_eval_counts(satisfy_positive_parse_review, "음식")
kindness_emo = emotion_eval_counts(satisfy_positive_parse_review, "친절")
# 가격 부정 리뷰글 빈도 높은 명사 갯수
buffet_emo = emotion_eval_counts(price_negative_parse_review, "뷔페")
food_neg_emo = emotion_eval_counts(price_negative_parse_review, "음식")
dessert_neg_emo = emotion_eval_counts(price_negative_parse_review, "디저트")noun_emote_bar(dessert_pos_emo, food_pos_emo, kindness_emo, "디저트","음식", "친절", "긍정 리뷰글 빈도 높은 명사")- 출력값

noun_emote_bar(buffet_emo,food_neg_emo,dessert_neg_emo,"뷔페","음식","디저트","부정 리뷰글 빈도 높은 명사")- 출력값

빈도 수 높은 명사들의 긍/부정 비율 시각화
def percent_emotion(reviews, total_number):
"""퍼센트 구하는 함수"""
return reviews / total_number * 100# 원그래프
import matplotlib.pyplot as plt
def pie_chart(positive, negative, keyword):
"""항목 2개인 Pie Chart 만드는 함수"""
ratio = [positive, negative]
labels = ["긍정", "부정"]
colors = ["#00539C", "#EEA47F"]
plt.title("{} 긍/부정 비율".format(keyword))
plt.pie(ratio, labels= labels, autopct='%.1f%%', colors = colors)
plt.show()만족도긍정 리뷰글 빈도 높은 명사들 긍/부정 비율 시각화
# 긍/부정 총 갯수 구하기
dessert_pos_total= dessert_pos_emo["긍정"] + dessert_pos_emo["부정"]
food_pos_total=food_pos_emo["긍정"] + food_pos_emo["부정"]
kindness_total = kindness_emo["긍정"] + kindness_emo["부정"]# 비율 구하기
dessert_positive_pos = percent_emotion(dessert_pos_emo["긍정"], dessert_pos_total)
food_positive_pos = percent_emotion(food_pos_emo["긍정"], food_pos_total)
kindness_pos = percent_emotion(kindness_emo["긍정"], kindness_total)
dessert_positive_neg = percent_emotion(dessert_pos_emo["부정"], dessert_pos_total)
food_positive_neg = percent_emotion(food_pos_emo["부정"], food_pos_total)
kindness_neg = percent_emotion(kindness_emo["부정"], kindness_total)pie_chart(dessert_positive_pos, dessert_positive_neg, "디저트")
pie_chart(food_positive_pos, food_positive_neg, "음식")
pie_chart(kindness_pos,kindness_neg,"친절")- 출력값



가격부정 리뷰글 빈도 높은 명사들 긍/부정 비율
# 부정 리뷰글 총 긍/부정 갯수 구하기
buffet_total = buffet_emo["긍정"] + buffet_emo["부정"]
food_neg_total = food_neg_emo["긍정"] + food_neg_emo["부정"]
dessert_neg_total = dessert_neg_emo["긍정"] + dessert_neg_emo["부정"]# 비율 구하기
buffet_pos = percent_emotion(buffet_emo["긍정"], buffet_total)
food_negative_pos = percent_emotion(food_neg_emo["긍정"], food_neg_total)
dessert_negative_pos = percent_emotion(dessert_neg_emo["긍정"], dessert_neg_total)
buffet_neg = percent_emotion(buffet_emo["부정"], buffet_total)
food_negative_neg = percent_emotion(food_neg_emo["부정"], food_neg_total)
dessert_negative_neg = percent_emotion(dessert_neg_emo["부정"], dessert_neg_total)pie_chart(buffet_pos, buffet_neg,"뷔페")
pie_chart(food_negative_pos, food_negative_neg, "음식")
pie_chart(dessert_negative_pos, dessert_negative_neg, "디저트")- 출력값


