이전에 수집한 댓글 데이터를 확률 예측의 변수로 만드는 과정에서 많은 고민이 있었다. 그 과정을 간단히 설명하자면, 댓글 내용을 형태소별로 토큰화하여 정식연재와 비정식연재 웹툰의 빈도수가 높은 단어들을 비교하였다. 빈도수가 높은 단어들 중 정식 연재를 판가름할 수 있다고 생각하는 단어들(ex. 재밌, 기대, 응원 등등)을 선택하여 전체 댓글과의 등장 비율을 계산하였다. 최종적으로 정식연재에 2배 많이 나타나는 단어와 비정식연재에 2배 많이 나타나는 단어를 필터링하여 등장 횟수를 변수로 넣었다.
아래는 이전에 수집한 'mycomment_data.csv'의 comment 칼럼의 예시이다.
# 영어 글자 확인
pd.set_option('display.max_rows', None)
## 정규표현식을 이용해 영어 외의 글자들 삭제
tmp = commentData['comment'].replace('[^a-zA-Z]', '', regex=True)
## 영어가 포함된 댓글들 출력
tmp[tmp.str.contains('[a-zA-Z]')]
영어가 들어있는 댓글들의 내용을 확인 후 대부분의 영단어가 큰 의미를 가지고 있지 않다고 판단했다. 몇몇 글자들을 제외한 영어 글자를 삭제하였다.
# bb -> 굿, zz -> ㅋㅋ
commentData['comment'] = commentData['comment'].str.replace("Good","굿")
commentData['comment'] = commentData['comment'].str.replace("bb+","굿")
commentData['comment'] = commentData['comment'].str.replace("zz+","ㅋㅋ")
# 나머지 영어 삭제
commentData['comment'] = commentData['comment'].replace('[a-zA-Z]','',regex=True)
이모티콘의 경우, 종류는 다양하지만 같은 의미를 담고 있는 것들을 묶어 한글로 대체하였다. '❤️'는 한글로 대체하기 애매하다고 생각하여 하나의 이모티콘으로 통일시켰다.
# 이모티콘 대체
commentData['comment'] = commentData['comment'].str.replace('[💟♡♥️❤❤️💓💕💖💗💘💙💚💛💜💝💞😍😘😻🤍🤎🥰🧡😚💋]+','❤️',regex=True)
commentData['comment'] = commentData['comment'].str.replace('[🙃🤣😆😀😊😄🤭😁😂]+','ㅋㅋ',regex=True)
commentData['comment'] = commentData['comment'].str.replace('[😢😭🥺]+','ㅠㅠ',regex=True)
commentData['comment'] = commentData['comment'].str.replace('[🔥👊💪]+','파이팅',regex=True)
commentData['comment'] = commentData['comment'].str.replace('[👍🏻👍]+','굿',regex=True)
commentData['comment'] = commentData['comment'].str.replace('[🎊🎉✨👏🥳💐]+','축하',regex=True)
commentData['comment'] = commentData['comment'].str.replace('[☆★⭐]+','별',regex=True)
commentData['comment'] = commentData['comment'].str.replace('[🍪]+','쿠키',regex=True)
commentData['comment'] = commentData['comment'].str.replace('[🙏]+',"제발",regex=True)
자음은 대부분 의미를 가지고 있지만, 후의 토큰화 과정에서는 그 의미를 캐치하지 못했다. 따라서 직접 의미를 풀어서 대체하였다.
# 자음 정리
commentData['comment'] = commentData['comment'].str.replace('ㄷㄷ+','덜덜',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㅎㅇㅌ','파이팅',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㅇㅈ','인정',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㄹㅈ','인정',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㄹㅈㄷ','레전드',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㄱㅇㅇ','귀여워',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㄷㄱ','두근',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㅊㅎ','축하',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㅊㅊ','축하',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㅆㄹㄱ','쓰레기',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㅁㄹ','몰라',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㄱㅊ','괜찮',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㅁㅊ','미친',regex=True)
# ㅎㅎ, ㅋㅋ 는 댓글마다 그 글자수가 달라 통일시킴.
commentData['comment'] = commentData['comment'].str.replace('ㅎㅎ+','ㅎㅎ',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㅋㅋ+','ㅋㅋ',regex=True)
모음의 경우 'ㅜㅜ'와 'ㅠㅠ' 외에는 대부분 오타로 보였다. 따라서 글자수만 통일시켜 주었다.
# 모음 정리
commentData['comment'] = commentData['comment'].str.replace('ㅜ+','ㅠㅠ',regex=True)
commentData['comment'] = commentData['comment'].str.replace('ㅠㅠ+','ㅠㅠ',regex=True)
# 한국어, 숫자, 띄어쓰기, 하트 제외 삭제
commentData['comment'] = commentData['comment'].str.replace('[^0-9ㅋㅎㅠ가-힣❤️ ]','',regex=True)
# 자음과 모음 앞뒤에 띄어쓰기를 넣어 별개의 단어로 판단하도록 함.
commentData['comment'] = commentData['comment'].str.replace('ㅋㅋ',' ㅋㅋ ')
commentData['comment'] = commentData['comment'].str.replace('ㅎㅎ',' ㅎㅎ ')
commentData['comment'] = commentData['comment'].str.replace('ㅠㅠ',' ㅠㅠ ')
commentData['comment'] = commentData['comment'].str.replace(' +',' ', regex=True)
## 의미가 없는 댓글 삭제
noMean = []
for i in range(len(commentData)):
if commentData.loc[i,'comment'] == '': noMean.append(i)
elif commentData.loc[i,'comment'] == ' ': noMean.append(i)
commentData.drop(noMean, inplace=True)
commentData.reset_index(drop=True, inplace=True)
댓글의 경우, 맞춤법이나 띄어쓰기가 정확하게 갖추어지지 않기 때문에 토큰화 전에 교정 과정이 필요했다. 한국어 교정을 해주는 'hanspell' 패키지를 이용하였다.
# hanspell 설치
pip install git+https://github.com/ssut/py-hanspell.git
from hanspell import spell_checker
for i in range(len(commentData)):
commentData.loc[i,'comment'] = spell_checker.check(commentData.loc[i,'comment']).checked
konlpy 의 okt, kkma, komoran, mecab 을 이용해보았다. mecab의 경우 윈도우에서 지원하지 않기 때문에 코랩에 설치 후 확인하였다.
# mecab 설치
!git clone https://github.com/SOMJANG/Mecab-ko-for-Google-Colab.git
%cd Mecab-ko-for-Google-Colab
!bash install_mecab-ko_on_colab190912.sh
## -> 2022년 11월에 코드 실행함. 2023년 4월에 확인 결과 오류가 뜨므로 수정 필요함.
# 원래 문장
original_sentence = commentData.loc[20,'comment']
print(original_sentence)
## '벌레는 진짜아니지이그래도 짜파게티에 계란과 군만두는 뭐너무 맛있어보이잖아요오나 한입만❤️'
# Okt
from konlpy.tag import Okt
okt = Okt()
print(*okt.morphs(original_sentence, stem=True))
## 벌레 는 진짜 아 니지 이 그래도 짜파게티 에 계란 과 군 만두 는 뭐 너무 맛있다 보이다 오 나 한 입 만 ❤️
# Kkma
from konlpy.tag import Kkma
kkma = Kkma()
print(*kkma.morphs(original_sentence))
## 벌레 는 진짜 알 니 지이 그리하 여도 짜 아 파 게 티 에 계란 과 군만두 는 뭐 너무 맛있 어 보이 잖아요 오 나 한입 만 ❤️ ️
# Komoran -> 코드 진행 시간이 너무 오래 걸려 제외함.
from konlpy.tag import Komoran
komoran = Komoran()
print(*komoran.morphs(original_sentence))
## 벌레 는 진짜 아니 지이 그래도 짜파게티 에 계란 과 군만두 는 뭐 너무 맛있 어 보이 잖아요 오 나 한입 만 ❤️ ️
# Mecab
from konlpy.tag import Mecab
mecab = Mecab()
print(*mecab.morphs(original_sentence))
## 벌레 는 진짜 아니 지이 그래도 짜파게티 에 계란 과 군만두 는 뭐 너무 맛있 어 보이 잖아요 오 나 한입 만 ❤️ ️
# 맞춤법 교정
spelled_sentence = spell_checker.check(original_sentence).checked
print(spelled_sentence)
## '벌레는 진짜 아니지 이 그래도 짜파게티에 계란과 군만두는 뭐 너무 맛있어 보이잖아요 오나 한입만❤️'
# 교정 후 Okt
print(*okt.morphs(spelled_sentence, stem=True))
## 벌레 는 진짜 아니다 이 그래도 짜파게티 에 계란 과 군 만두 는 뭐 너무 맛있다 보이다 오 나 한 입 만 ❤️
# 교정 후 Kkma
print(*kkma.morphs(spelled_sentence))
## 벌레 는 진짜 아니 지 이 그리하 여도 짜 아 파 게 티 에 계란 과 군만두 는 뭐 너무 맛있 어 보이 잖아요 오 나 한입 만 ❤️
# 교정 후 Mecab
print(*kkma.morphs(spelled_sentence))
## 벌레는 진짜 아니지 이 그래도 짜파게티에 계란과 군만두는 뭐 너무 맛있어 보이잖아요 오나 한입만❤️
형태소 분석기 | 출력 결과 |
---|---|
원래 문장 | 벌레는 진짜아니지이그래도 짜파게티에 계란과 군만두는 뭐너무 맛있어보이잖아요오나 한입만❤️ |
haspell 교정 | 벌레는 진짜 아니지 이 그래도 짜파게티에 계란과 군만두는 뭐 너무 맛있어 보이잖아요 오나 한입만❤️ |
Okt | 벌레 는 진짜 아니다 이 그래도 짜파게티 에 계란 과 군 만두 는 뭐 너무 맛있다 보이다 오 나 한 입 만 ❤️ |
Kkma | 벌레 는 진짜 아니 지 이 그리하 여도 짜 아 파 게 티 에 계란 과 군만두 는 뭐 너무 맛있 어 보이 잖아요 오 나 한입 만 ❤️ |
Mecab | 벌레는 진짜 아니지 이 그래도 짜파게티에 계란과 군만두는 뭐 너무 맛있어 보이잖아요 오나 한입만❤️ |
위 형태소 분석기 중 mecab이 가장 효율적이라고 판단하였다.
Mecab을 이용하여 형태소별로 문장을 쪼갰다. 각 형태소 중 조사, 어미 등 의미없는 단어는 제외하며 토큰화를 진행하였다. 정식연재와 비정식연재의 단어들을 워드 클라우드 형태로 시각화하였다.
# import
!git clone https://github.com/SOMJANG/Mecab-ko-for-Google-Colab.git
%cd Mecab-ko-for-Google-Colab
!bash install_mecab-ko_on_colab190912.sh
from konlpy.tag import Mecab
mecab = Mecab()
from tensorflow.keras.preprocessing.text import Tokenizer
from wordcloud import WordCloud
import matplotlib.pyplot as plt
!apt-get update -qq
!apt-get install fonts-nanum* -qq
import matplotlib.font_manager as fm
sys_font = fm.findSystemFonts()
# 체언, 용언(동사, 형용사), 일반부사, 감탄사, 체언 접두사, 어근, 부호 및 숫자
goodPos = ['NNG','NNP','NNBC','NR','NP','VV','VA','MAG','IC','XPN','XR']
# 정식연재 토큰화
pubCom = commentData[commentData['isPublic'] == 1]
public_mecab = []
for sentence in pubCom['comment']:
tokenized_sentence = mecab.pos(sentence)
token = []
for i in range(len(tokenized_sentence)):
if tokenized_sentence[i][1] in goodPos:
token.append(tokenized_sentence[i][0])
elif tokenized_sentence[i][1][:2] in ['VV','VA']: # 동사와/형용사 + 어미
token.append(tokenized_sentence[i][0])
public_mecab.append(token)
tokenizer = Tokenizer()
tokenizer.fit_on_texts(public_mecab)
wordDict = tokenizer.word_counts
wordDict_sorted = list(sorted(tokenizer.word_counts.items(), key=lambda x: x[1], reverse=True))
len(wordDict_sorted)
wc = WordCloud(font_path='/usr/share/fonts/truetype/nanum/NanumGothic.ttf', background_color='white')
gen = wc.generate_from_frequencies(wordDict)
plt.figure()
plt.imshow(gen)
plt.axis('off')
# 비정식연재 토큰화
notPubCom = commentData[commentData['isPublic'] == 0]
notPublic_mecab = []
for sentence in notPubCom['comment']:
tokenized_sentence = mecab.pos(sentence)
token = []
for i in range(len(tokenized_sentence)):
if tokenized_sentence[i][1] in goodPos:
token.append(tokenized_sentence[i][0])
elif tokenized_sentence[i][1][:2] in ['VV','VA']:
token.append(tokenized_sentence[i][0])
notPublic_mecab.append(token)
tokenizer2 = Tokenizer()
tokenizer2.fit_on_texts(notPublic_mecab)
wordDict2 = tokenizer2.word_counts
wordDict_sorted2 = list(sorted(tokenizer2.word_counts.items(), key=lambda x: x[1], reverse=True))
print(len(wordDict_sorted2))
print(wordDict_sorted2)
wc = WordCloud(font_path='/usr/share/fonts/truetype/nanum/NanumGothic.ttf', background_color='white')
gen = wc.generate_from_frequencies(wordDict2)
plt.figure()
plt.imshow(gen)
plt.axis('off')
정식연재와 비정식연재 웹툰의 순위권 단어들이 비슷하게 나온다. 따라서 비율을 고려해 보았다.
# 전체 댓글 토큰화
commentData2 = commentData.copy()
for i in range(len(commentData)):
sentence = commentData.loc[i,'comment']
if type(sentence) == float: continue
tokenized_sentence = mecab.pos(sentence)
token = ''
for j in range(len(tokenized_sentence)):
if tokenized_sentence[j][1] in goodPos:
token += ' '+tokenized_sentence[j][0]
elif tokenized_sentence[j][1][:2] in ['VV','VA']: # 체언접두사와 어근
token += ' '+tokenized_sentence[j][0]
commentData2.loc[i,'comment'] = token
# 정식과 비정식을 나눌 수 있다고 판단되는 들
wordsList = ['가', '가셨으면', '가즈아', '감동', '감성', '감정', '갑시다', '개성', '계속', '고침', '고퀄', '공감', '괜찮', '굿', '궁금', '귀여', '귀여우', '귀여운', '귀여움', '귀여워', '귀여워서', '귀염', '귀엽', '그리', '그림', '기다렸', '기다리', '기대', '깜찍', '꾸준히', '나쁜', '네이버', '다음', '담당자', '답답', '대박', '대작', '더', '덜', '데려가', '독특', '두근두근', '드디어', '등록', '디테일', '따뜻', '매력', '매주', '명작', '모셔', '몰입', '무서워', '무섭', '미쳤', '미친', '반갑', '발암', '베스트', '별로', '부들부들', '분량', '분위기', '비슷', '빨리', '사이다', '새로', '새로운', '색감', '생각', '생각나', '설레', '세계관', '소름', '소원', '소재', '스타일', '스토리', '승격', '시키', '신기', '신선', '실화', '싫', '아쉽', '알림', '어서', '얼른', '연재', '연출', '열심히', '옆', '예뻐요', '예쁘', '예쁜', '오랜만', '오지', '올라가', '올려', '올리', '완결', '웃', '웃겨', '웃겨요', '웃기', '원합니다', '위', '응원', '이뻐요', '이쁘', '이상', '이야기', '작품', '작화', '장면', '재미', '재미나', '재미있', '재밌', '잼', '전개', '정식', '좋', '좋아하', '주인공', '주제', '진심', '짧', '쩔', '참신', '처음', '최강', '최고', '추천', '축하', '취향', '친구', '캐릭터', '쿠키', '퀄리티', '탄탄', '파이팅', '팬', '표절', '표정', '피드백', '헉', '현기증', '현실', '흑흑', '흥미', '흥미진진', '힐링', '힘내']
# 같은 의미의 단어들 하나로 통일
commentData2['comment'] = commentData2['comment'].str.replace('가셨으면','가')
commentData2['comment'] = commentData2['comment'].str.replace('갑시다','가')
commentData2['comment'] = commentData2['comment'].str.replace('귀여우','귀엽')
commentData2['comment'] = commentData2['comment'].str.replace('귀여운','귀엽')
commentData2['comment'] = commentData2['comment'].str.replace('귀여움','귀엽')
commentData2['comment'] = commentData2['comment'].str.replace('귀여워서','귀엽')
commentData2['comment'] = commentData2['comment'].str.replace('귀염','귀엽')
commentData2['comment'] = commentData2['comment'].str.replace('귀여워','귀엽')
commentData2['comment'] = commentData2['comment'].str.replace('귀여','귀엽')
commentData2['comment'] = commentData2['comment'].str.replace('그리','그림')
commentData2['comment'] = commentData2['comment'].str.replace('기다렸','기다리')
commentData2['comment'] = commentData2['comment'].str.replace('무서워','무섭')
commentData2['comment'] = commentData2['comment'].str.replace('미친','미쳤')
commentData2['comment'] = commentData2['comment'].str.replace('예뻐요','예쁘')
commentData2['comment'] = commentData2['comment'].str.replace('예쁜','예쁘')
commentData2['comment'] = commentData2['comment'].str.replace('올라가','올려')
commentData2['comment'] = commentData2['comment'].str.replace('올리','올려')
commentData2['comment'] = commentData2['comment'].str.replace('웃','웃겨')
commentData2['comment'] = commentData2['comment'].str.replace('웃겨요','웃겨')
commentData2['comment'] = commentData2['comment'].str.replace('웃기','웃겨')
commentData2['comment'] = commentData2['comment'].str.replace('이뻐요','예쁘')
commentData2['comment'] = commentData2['comment'].str.replace('이쁘','예쁘')
commentData2['comment'] = commentData2['comment'].str.replace('재미나','재미')
commentData2['comment'] = commentData2['comment'].str.replace('재미있','재미')
commentData2['comment'] = commentData2['comment'].str.replace('재밌','재미')
commentData2['comment'] = commentData2['comment'].str.replace('잼','재미')
commentData2['comment'] = commentData2['comment'].str.replace('좋','좋아하')
commentData2['comment'] = commentData2['comment'].str.replace('흥미진진','흥미')
# 한 댓글 내에 중복등장하는 단어 정리
for i in range(len(commentData2)):
comSet = set(commentData2.loc[i,'comment'].split(' '))
token = ''
for j in range(1,len(comSet)):
token += ' '+list(comSet)[j]
commentData2.loc[i,'comment'] = token
# 정식연재와 비정식연재 웹툰의 단어들 빈도 비율 비교
token_ratio = pd.DataFrame(arr, columns=['public','notPublic'])
for i in range(len(commentData2)):
tokenList = commentData2.loc[i,'comment'].split(' ')[1:]
for word in token_ratio.index:
if word in tokenList:
if commentData2.loc[i,'isPublic'] == 1:
token_ratio.loc[word,'public'] += 1/(len(pubId)*6)
elif commentData.loc[i,'isPublic'] == 0:
token_ratio.loc[word,'notPublic'] += 1/(len(notPubId)*6)
# 정식연재가 2배 많은 단어
words1 = token_ratio[token_ratio['public'] > token_ratio['notPublic'] * 2]
print(words1.index)
## '고침', '나쁜', '담당자', '덜', '데려가', '두근두근', '등록', '디테일', '매주', '모셔', '무섭', '미쳤', '발암', '부들부들', '사이다', '새로', '색감', '설레', '소름', '소원', '시키', '얼른', '옆', '오지', '위', '작화', '전개', '쩔', '최강', '친구', '쿠키', '탄탄', '표절', '피드백', '헤어지', '현기증', '현실'
# 비정식연재가 2배 많은 단어
words0 = token_ratio[token_ratio['public'] * 2 < token_ratio['notPublic']]
print(words0.index)
## '감동', '감성', '감정', '개성', '깜찍', '명작', '비슷', '새로운', '소식', '신기', '아쉽', '오랜만', '완결', '원합니다', '이야기', '장면', '짧', '추천', '축하'
최종 단어 선택! 위의 각 단어들 중 더더욱 정식연재 여부를 구별할 수 있는 단어들을 선택하였다.
- 정식 연재 단어 words1
'나쁜', '데려가', '두근두근', '등록', '디테일', '매주', '무섭', '미쳤', '발암', '부들부들', '사이다', '색감', '소름', '얼른', '옆', '오지', '작화', '전개', '쩔', '쿠키', '탄탄', '현실'
- 비정식 연재 단어 words0
'감동', '명작', '비슷', '새로운', '소식', '아쉽', '오랜만', '완결', '이야기', '짧', '축하'
이제, 각 웹툰별로 정식연재와 비정연재 단어들이 몇번 나타나는지를 카운트하여 그 숫자를 값으로 넣었다.
titleId = commentData2.drop_duplicates('titleId')['titleId']
commentData3 = pd.DataFrame(columns= ['titleId','words0','words1'])
commentData3['titleId'] = titleId
commentData3.reset_index(drop=True, inplace=True)
for i in range(len(commentData3)):
comment_dt = commentData2['comment'][commentData2['titleId'] == commentData3.loc[i,'titleId']]
w0 = 0; w1 = 0
for com in comment_dt:
for word in words0:
if word in com: w0 += 1
for word in words1:
if word in com: w1 += 1
commentData3.loc[i,'words0'] = w0
commentData3.loc[i,'words1'] = w1
commentData3.to_csv('comment_words.csv', index=False)