베스트도전 웹툰의 정식연재 승격 확률 예측 - 4. 댓글분석

조은진·2023년 4월 8일
0
post-thumbnail

이전에 수집한 댓글 데이터를 확률 예측의 변수로 만드는 과정에서 많은 고민이 있었다. 그 과정을 간단히 설명하자면, 댓글 내용을 형태소별로 토큰화하여 정식연재와 비정식연재 웹툰의 빈도수가 높은 단어들을 비교하였다. 빈도수가 높은 단어들 중 정식 연재를 판가름할 수 있다고 생각하는 단어들(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 형태소 분석, 토큰화

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)
profile
열심히 노력하는 학생!

0개의 댓글