세부 과제

  1. README.md - 프로젝트의 목적과 진행과정에 대해 작성한다.
  2. 크롤링을 통한 데이터 수집
  • 재사용이 가능하도록 함수화
  • 원본 데이터 파일 저장
  1. 데이터 전처리
  1. 필요없는 문자 제거
    • 방법 1. 맞춤법 검사 / 띄어쓰기 교정
    • 방법 2. 불용어 제거
    • 방법 3. 정규표현식을 통한 특수문자 및 자음/모음 제거
  2. 리뷰의 길이 분포 확인
    • 통계요약표 또는 히스토그램으로 확인 각 전처리의 선택 과정을 표현해주세요.

전처리 과정의 필요성

Tokenizing(토큰나이징) : 텍스트를 어떤 단어로 쪼개는 과정 Imbedding(임베딩) : tokenizing된 단어들을 숫자 벡터로 만드는 과정 쪼개진 단어가 곧 하나의 숫자로 변하게 되는 것

1.필요없는 문자 제거

  • 텍스트 분석은 문장에서 단어들의 배치 패턴을 파악하여 문맥을 이해

    • ex) 문법에서 주어 다음 동사가 온다는 것을 학습을 통해 이해,
      • 새로운 주어 다음에 동사를 예측하는 것
  • 패턴을 잘 파악하기 위해서는 의미가 있는 단어들의 모음으로 벡터가 제작되어야함

  • 행렬은 크기가 클수록 연산처리가 많아지고, 속도도 느려짐

  1. 리뷰의 길이 분포 확인
  • 분석을 하기 위해서는 행렬 연산이 되어야 하기 때문에 리뷰의 길이가 다르더라도 임베딩되는 숫자 벡터의 길이는 동일하게 만듦
  • 길이가 짧은 리뷰의 경우에는 임베딩할 길이를 맞추기 위해 0으로 채움
  • 희소행렬(sparse matrix)는 메모리, 속도에 비해 비효율적이다. 길이가 너무 짧거나 긴 문장을 제외하는 이유
  • 자신이 가지고 있는 데이터를 보고, 어떤 방법이 데이터 분석을 하는데 유용할지 판단
  • 과정에 이유가 있어야 다른 사람들과의 차별점이 된다.

큰 틀의 과제

  1. 데이터 수집
  2. 데이터 전처리
  3. 데이터 분석

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. 계획 설정

  1. 전체 함수 이름 결정: get_review(url)
  2. 덩어리별로 분류:
    • 크롬 창 열고 url 접속하기: open_url(url), output: driver
    • 더보기 버튼을 눌러 총 리뷰 갯수 구하기: count_reviews(driver), output: rows, cnt
    • 리뷰 데이터를 추출하기: crawling_reviews(rows, cnt), output: review_data

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_reviews

2. 데이터 전처리

Reference

동국대 텍스트 전처리
한국어 불용어 처리 1
한국어 불용어 처리 2
위키독스
한국어 텍스트 전처리
Github정리
이모티콘 제거

  • 데이터전처리 방법은 작성자 마음이다.

데이터 전처리시 고려해야할 사항

  1. 특수문자, 초성기호 등 제거
    • re.sub()함수 쓰기
  2. 문장의 길이
    • 문장의 길이 이상치를 정한 다음 이를 넘기거나 혹은 이에 준하지 못하는 문장은 제거
  3. 맞춤법 / 띄어쓰기
    • 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_string

1. 명사 분류하기 및 시각화

# 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 results
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)
    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_results
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)
    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 ChatOpenAI
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_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_name
def 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, "디저트")
  • 출력값