프로젝트

성심당 리뷰를 이용한 긍정, 부정 워드클라우드 프로젝트

환성 2022. 12. 24. 16:01
728x90

프로젝트의 일환으로 성심당(대전의 유명한 빵집)의 리뷰들을 모아 워드클라우드를 만드는 작업을 했다.

코드 작성은 python으로 프로그램은 colab으로 구동했다

리뷰 데이터는 2022년 11월~ 2022년 3월의 리뷰를 인스타, 네이버로부터 추출하였고 인스타의 경우 2022년 3월까지 가지고 오는게 불가능하여 2022년 10월까지만 가지고 오게 되었다(크롤링 도중 봇으로 인식하여 자동으로 종료됨)

train set과 test set을 나눈뒤 test set은 NaverInstagram.csv, train set은 ratings_test.txt를 사용했다

 

총 데이터의 갯수는 6046개 그중 인스타그램은 1076개 네이버는 4970개이다.

우선 작업 도중 경고 메시지가 뜨지 않게 하기 위해서 다음과 같은 작업을 한다

# 경고 메시지 표시 X
import warnings
warnings.filterwarnings(action='ignore')

이후 데이터를 불러오는 작업을 한다

import pandas as pd

review_data = pd.read_csv('/NaverInstagram.csv', encoding='utf-8')
review_data.head()

데이터의 type 확인 및 이상이 없는지 확인한 뒤 date의 칼럼을 datetime64로 변경해야 한다

# 데이터 확인
review_data_train.info()

# 컬럼 데이터 타입 변경
review_data = review_data.astype({'date':'datetime64'})

이후 train할 데이터 셋을 업로드 해준뒤 train 데이터 셋의 content 칼럼이 Null인 샘플을 모두 제거한다.(결측치 제거)

# train 데이터 업로드
review_data_train = pd.read_csv('/ratings_test.txt', sep='\t')
review_data_train.head()

# content 칼럼 Null인 샘플 제거
review_data_train = review_data_train[review_data_train['content'].notnull()]

한글 이외의 문자들을 공백으로 변환해야 한다. 그 이유는 불필요한 문자나 공백이 있을경우 제대로 된 결과가 나오지 않을 수 있기 때문이다. 그 다음 타겟 컬럼의 label을 확인해준다.

# 한글 이외의 문자 공백으로 변환
import re
review_data_train['content'] = review_data_train['content'].apply(lambda x : re.sub(r'[^ ㄱ-ㅣ가-힣]+', "", x))

# 타겟 컬럼 label 확인
print(review_data_train['label'].value_counts())

이제부터 konlpy 패키지의 Okt(형태소 분석기)를 이용하여 형태소별로 나누는 과정을 한다.

from konlpy.tag import Okt

okt = Okt()

def okt_tokenizer(text):
    tokens = okt.morphs(text)
    return tokens

자연어 처리인 TfidVectorizer를 이용하여 벡터화를 해준다. TfidfVectorizer에서 두 가지를 이해해야 하는데 Tf, idf이다.

1) Tf(Term Frequency)

- 하나의 문장에서 특정 단어가 등장하는 횟수

2) Idf(Inverse Document Frequency) 

- Df는 문서의 빈도, 특정 단어가 몇 개의 문장에서 등장하는지를 수치화 한 것이고 그것에 역수를 취한 것이Idf이다.

역수를 취한 이유는 적은 문장에 등장할수록 값을 크게 하고 많은 문장에서 등장할수록 숫자를 작아지게 함으로써 여러 문장에서 의미 없이 사용되는 단어의 가중치를 줄이기 위해서이다.

Tfidf의 공식

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer(tokenizer = okt_tokenizer, ngram_range=(1,2), min_df=3, max_df=0.9)
tfidf.fit(review_data_train['content'])
review_data_train_tfidf = tfidf.transform(review_data_train['content'])

이후 회귀분석을 진행해준다. 진행 한 뒤 best parameter(최적의 모수)를 찾아 내는 작업을 진행한다.

from sklearn.linear_model import LogisticRegression

SA_lr = LogisticRegression(random_state = 0)

# best parameter 찾기
from sklearn.model_selection import GridSearchCV

params = {'C': [1, 3, 3.5, 4, 4.5, 5]}
SA_lr_grid_cv = GridSearchCV(SA_lr, param_grid=params, cv=3, scoring='accuracy', verbose=1)

#최적 분석
SA_lr_grid_cv.fit(review_data_train_tfidf, review_data_train['label'])

최적의 파라미터의 best 모델을 저장 한 뒤 분석 모델을 평가한다.

# 최적 파라미터의 best 모델 저장
SA_lr_best = SA_lr_grid_cv.best_estimator

# 분석 모델 평가
review_test_tfidf = tfidf.transform(review_data_train['content'])
test_predict = SA_lr_best.predict(review_test_tfidf)

분석할 데이터를 피처 벡터화 하는 과정이 필요하다. 우리가 필요한 데이터 칼럼은 content(내용)이므로 이 칼럼을 이용해서 감성 분석을 진행하는 것이다.

# 1) 분석할 데이터의 피처 벡터화 ---<< content >> 분석
review_data_tfidf = tfidf.transform(review_data['content'])

# 2) 최적 파라미터 학습모델에 적용하여 감성 분석
review_data_predict = SA_lr_best.predict(review_data_tfidf)

# 3) 감성 분석 결과값을 데이터 프레임에 저장
review_data['content_label'] = review_data_predict

# 4) csv 파일로 저장
review_data.to_csv('/review_data_predict.csv', encoding='utf-8') 

# 갯수 홧인
print(review_data['title_label'].value_counts())

마지막으로 감성분석을 진행한 결과 중 부정적 리뷰, 긍정적 리뷰를 나누는 작업을 진행해줘야 한다. content_label의 칼럼은 0과 1로 나누어 0의 경우 부정 1의 경우 긍정을 나타내는 표시할 수 있는 칼럼의 일환이다.

columns_name = ['date','source','content','content_label']
NEG_data_df = pd.DataFrame(columns=columns_name)
POS_data_df = pd.DataFrame(columns=columns_name)

for i, data in review_data.iterrows(): 
    date = data["date"] 
    source = data["source"]
    content = data["content"] 
    c_label = data["content_label"] 
    
    if c_label == 0: # 부정 감성 샘플만 추출
        NEG_data_df = NEG_data_df.append(pd.DataFrame([[date,source,content,c_label]],columns=columns_name),ignore_index=True)
    else : # 긍정 감성 샘플만 추출
        POS_data_df = POS_data_df.append(pd.DataFrame([[date,source,content,c_label]],columns=columns_name),ignore_index=True)
     
# 파일에 저장.
NEG_data_df.to_csv('/NES_SungSimdang.csv', encoding='utf-8') 
POS_data_df.to_csv('/POS_SungSimdang.csv', encoding='utf-8')

이후 감성 분석 결과를 시각화 하기 위해 긍정 감성 데이터에서만 명사를 추출한다.

# 감성 분석 결과 시각화
# 긍정 감성 데이터에서만 명사 추출
POS_description = POS_data_df['content']

POS_description_noun_tk = []
POS_description_noun_join = []

for d in POS_description:
    POS_description_noun_tk.append(okt.nouns(d)) #형태소가 명사인 것만 추출
    
for d in POS_description_noun_tk:
    d2 = [w for w in d if len(w) > 1] #길이가 1인 토큰은 제외
    POS_description_noun_join.append(" ".join(d2)) # 토큰을 연결(join)하여 리스트 구성

부정데이터의 경우도 마찬가지로 진행한다.

# 부정 감성의 데이터에서 명사만 추출
NEG_description = NEG_data_df['content']

NEG_description_noun_tk = []
NEG_description_noun_join = []

for d in NEG_description:
    NEG_description_noun_tk.append(okt.nouns(d)) #형태소가 명사인 것만 추출
    
for d in NEG_description_noun_tk:
    d2 = [w for w in d if len(w) > 1]  #길이가 1인 토큰은 제외
    NEG_description_noun_join.append(" ".join(d2)) # 토큰을 연결(join)하여 리스트 구성

마지막으로 긍정 dtm, 부정 dtm을 구성해준다.

# 긍정 dtm 구성
POS_tfidf = TfidfVectorizer(tokenizer = okt_tokenizer, min_df=2 )
POS_dtm = POS_tfidf.fit_transform(POS_description_noun_join)

POS_vocab = dict() 

for idx, word in enumerate(POS_tfidf.get_feature_names()):
    POS_vocab[word] = POS_dtm.getcol(idx).sum()
    
POS_words = sorted(POS_vocab.items(), key=lambda x: x[1], reverse=True)


# 부정 dtm 구성
NEG_tfidf = TfidfVectorizer(tokenizer = okt_tokenizer, min_df=2 )
NEG_dtm = NEG_tfidf.fit_transform(NEG_description_noun_join)

NEG_vocab = dict() 

for idx, word in enumerate(NEG_tfidf.get_feature_names()):
    NEG_vocab[word] = NEG_dtm.getcol(idx).sum()
    
NEG_words = sorted(NEG_vocab.items(), key=lambda x: x[1], reverse=True)

워드 클라우드로 시각화 하는 작업을 진행한다.

from wordcloud import WordCloud
import matplotlib.pyplot as plt
from collections import Counter
from konlpy.tag import Okt
from PIL import Image
import numpy as np
from wordcloud import STOPWORDS

with open('/POS_SungSimdang.csv', 'r', encoding='utf-8') as f:
    text = f.read()

okt = Okt()
nouns = okt.nouns(text) # 명사만 추출
stopwords = {'성심당'} # 불용어

words = [n for n in nouns if len(n) > 1] # 단어의 길이가 1개인 것은 제외

c = Counter(words) # 위에서 얻은 words를 처리하여 단어별 빈도수 형태의 딕셔너리 데이터를 구함


wc = WordCloud(font_path='/usr/share/fonts/truetype/nanum/NanumGothicEco.ttf', width=400, height=400, scale=2.0, max_font_size=250)
gen = wc.generate_from_frequencies(c)
plt.figure()
plt.imshow(gen)

결과 : 

전체 기간에만 국한된게 아닌 분기별로도 나누어 보았다.

네이버_1분기_긍정 및 부정리뷰(좌 : 긍정, 우 : 부정)

 

네이버_2분기_긍정 및 부정리뷰(좌 : 긍정, 우 : 부정)

 

네이버_3분기_긍정 및 부정리뷰(좌 : 긍정, 우 : 부정)

 

인스타그램_3분기_긍정 및 부정리뷰(좌 : 긍정, 우 : 부정)

 

 

전처리 파일 및 훈련 데이터 셋 : 

 

NaverInstagram.csv
0.58MB

 

ratings_test.txt
4.67MB