<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>goodjin_55.log</title>
        <link>https://velog.io/</link>
        <description>열심히 노력하는 학생!</description>
        <lastBuildDate>Sat, 08 Apr 2023 16:59:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. goodjin_55.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/goodjin_55" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[베스트도전 웹툰의 정식연재 승격 확률 예측 - 5. 모델링]]></title>
            <link>https://velog.io/@goodjin_55/%EB%B2%A0%EC%8A%A4%ED%8A%B8%EB%8F%84%EC%A0%84-%EC%9B%B9%ED%88%B0%EC%9D%98-%EC%A0%95%EC%8B%9D%EC%97%B0%EC%9E%AC-%EC%8A%B9%EA%B2%A9-%ED%99%95%EB%A5%A0-%EC%98%88%EC%B8%A1-5.-%EB%AA%A8%EB%8D%B8%EB%A7%81</link>
            <guid>https://velog.io/@goodjin_55/%EB%B2%A0%EC%8A%A4%ED%8A%B8%EB%8F%84%EC%A0%84-%EC%9B%B9%ED%88%B0%EC%9D%98-%EC%A0%95%EC%8B%9D%EC%97%B0%EC%9E%AC-%EC%8A%B9%EA%B2%A9-%ED%99%95%EB%A5%A0-%EC%98%88%EC%B8%A1-5.-%EB%AA%A8%EB%8D%B8%EB%A7%81</guid>
            <pubDate>Sat, 08 Apr 2023 16:59:03 GMT</pubDate>
            <description><![CDATA[<p>정식연재 웹툰의 수가 비정식연재 웹툰의 수에 비해 매우매우 적어, 정확도만으로는 정확한 분류와 예측이 힘들었다. 따라서 분류의 명확성을 나타내는 AUC를 모델의 성능으로 선택하였다.</p>
<h1 id="import">import</h1>
<pre><code class="language-python">!pip install imbalanced-learn

import numpy as np
import pandas as pd

import sklearn
from sklearn import metrics
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score, roc_auc_score, roc_curve
import statsmodels.api as sm
import matplotlib.pyplot as plt
import time

from collections import Counter
from imblearn.under_sampling import RandomUnderSampler</code></pre>
<h1 id="🔎-데이터-확인">🔎 데이터 확인</h1>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/d74a537a-419f-450c-834d-cdf7cd2d33cc/image.png" alt=""></p>
<pre><code># class imbalance 확인
y.sum()/len(y)
## 0.07203389830508475</code></pre><p>데이터 불균형이 매우 심한 것을 알 수 있다.</p>
<pre><code class="language-python"># titleId 칼럼 제거
webtoon.drop([&quot;titleId&quot;], axis=1, inplace = True)</code></pre>
<h1 id="🌏-모델링">🌏 모델링</h1>
<h2 id="원핫인코딩">원핫인코딩</h2>
<pre><code class="language-python"># contentGenre, typeGenre : one-hot encoding
webtoon = pd.get_dummies(data = webtoon, columns=[&#39;contentGenre&#39;], prefix=&#39;contGenre&#39;)
webtoon = pd.get_dummies(data = webtoon, columns=[&#39;typeGenre&#39;], prefix=&#39;typeGenre&#39;)</code></pre>
<h2 id="언더샘플링--랜덤포레스트분류">언더샘플링 + 랜덤포레스트분류</h2>
<pre><code class="language-python">accList = []
aucList = []

# X,y dataset 분리
X, y = webtoon.iloc[:,:-1], webtoon[&#39;isPublic&#39;]

# 4:3 언더샘플링
undersample = RandomUnderSampler(sampling_strategy=0.7, random_state=121818)
X_under, y_under = undersample.fit_resample(X, y)

for i in range(50): # 50번 수행
    ## train, test split
    X_train_under, X_test_under, y_train_under, y_test_under = train_test_split(X_under, y_under, stratify=y_under, test_size=0.2, random_state=121818)

    ## 랜덤포레스트분류
    rf = RandomForestClassifier(random_state=121818)
    rf.fit(X_train_under, y_train_under)

    ## 정확도 acc
    accList.append(rf.score(X_test_under,y_test_under))
    ## 오차행렬
    y_pred = rf.predict(X_test_under)
    cfmat = confusion_matrix(y_test_under, y_pred)
    ## auc
    y_pred_proba = rf.predict_proba(X_test_under)[:,1] # -&gt; 정식연재 승격 확률
    auc = roc_auc_score(y_test_under, y_pred_proba)
    aucList5.append(auc)</code></pre>
<blockquote>
<ul>
<li>모델 성능 확인
AUC : 0.9285
ACC : 0.7812
confusion matrix : $\begin{bmatrix}10&amp;8\1&amp;13\ \end{bmatrix}$</li>
<li><blockquote>
<p>정식연재를 정식연재로 판단 10개, 정식연재를 비정식연재로 판단 8개, 비정식연재를 정식연재로 판단 1개, 비정식연재를 비정식연재로 판단 13개</p>
</blockquote>
</li>
</ul>
</blockquote>
<h2 id="변수-중요도">변수 중요도</h2>
<pre><code class="language-python">#변수 중요도 확인
feature_df = pd.DataFrame()
feature_df[&#39;feature&#39;] = X.columns
feature_df[&#39;importance&#39;] = rf_under.feature_importances_
feature_df</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/ea5a3a6a-47ee-46c9-bbcd-83ddd65535ca/image.png" alt=""></p>
<p>장르 변수들이 중요도가 매우 낮다. 그러나, 장르 변수를 제외하고 모델링한 결과는 성능이 더 안좋았다.</p>
<p>❗ 별점참여자수의 비율과 조회수의 비율이 타 변수들에 비해 중요도가 크게 나왔다. 정식연재로 승격될 확률은 유입독자들을 잘 유지하면 높아질 것으로 예상할 수 있었다. </p>
<h1 id="🥂-느낀점">🥂 느낀점</h1>
<p>데이터 수집부터 전처리, 모델링을 직접 하려니 며칠을 밤을 샜는지 모르겠다..ㅠ 많이 부족한 실력으로 욕심은 많아서 고생을 좀 했지만, 모든 과정들을 이해하고 적용하는 것이 큰 공부가 되었다. 스스로 처음부터 끝까지 해냈다는 것이 뿌듯하다.</p>
<p>중간중간 생각만 하고 진행하지 못했던 부분들(ex. 댓글분석에서 단어 선택, 변수의 상관관계 확인)을 더 보완하여 더 멋진 프로젝트를 진행할 것이다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[베스트도전 웹툰의 정식연재 승격 확률 예측 - 4. 댓글분석]]></title>
            <link>https://velog.io/@goodjin_55/%EB%B2%A0%EC%8A%A4%ED%8A%B8%EB%8F%84%EC%A0%84-%EC%9B%B9%ED%88%B0%EC%9D%98-%EC%A0%95%EC%8B%9D%EC%97%B0%EC%9E%AC-%EC%8A%B9%EA%B2%A9-%ED%99%95%EB%A5%A0-%EC%98%88%EC%B8%A1-4.-%EB%8C%93%EA%B8%80%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@goodjin_55/%EB%B2%A0%EC%8A%A4%ED%8A%B8%EB%8F%84%EC%A0%84-%EC%9B%B9%ED%88%B0%EC%9D%98-%EC%A0%95%EC%8B%9D%EC%97%B0%EC%9E%AC-%EC%8A%B9%EA%B2%A9-%ED%99%95%EB%A5%A0-%EC%98%88%EC%B8%A1-4.-%EB%8C%93%EA%B8%80%EB%B6%84%EC%84%9D</guid>
            <pubDate>Sat, 08 Apr 2023 16:23:11 GMT</pubDate>
            <description><![CDATA[<p>이전에 수집한 댓글 데이터를 확률 예측의 변수로 만드는 과정에서 많은 고민이 있었다. 그 과정을 간단히 설명하자면, 댓글 내용을 형태소별로 토큰화하여 정식연재와 비정식연재 웹툰의 빈도수가 높은 단어들을 비교하였다. 빈도수가 높은 단어들 중 정식 연재를 판가름할 수 있다고 생각하는 단어들(ex. 재밌, 기대, 응원 등등)을 선택하여 전체 댓글과의 등장 비율을 계산하였다. 최종적으로 정식연재에 2배 많이 나타나는 단어와 비정식연재에 2배 많이 나타나는 단어를 필터링하여 등장 횟수를 변수로 넣었다.</p>
<h1 id="🔎-댓글-데이터-예시">🔎 댓글 데이터 예시</h1>
<p>아래는 이전에 수집한 &#39;mycomment_data.csv&#39;의 comment 칼럼의 예시이다.
<img src="https://velog.velcdn.com/images/goodjin_55/post/0f5ba3e3-cdd8-43ec-8206-dce474d53b31/image.png" alt=""></p>
<h1 id="📄-글자-정리">📄 글자 정리</h1>
<h2 id="영어">영어</h2>
<pre><code class="language-python"># 영어 글자 확인
pd.set_option(&#39;display.max_rows&#39;, None)
## 정규표현식을 이용해 영어 외의 글자들 삭제
tmp = commentData[&#39;comment&#39;].replace(&#39;[^a-zA-Z]&#39;, &#39;&#39;, regex=True)
## 영어가 포함된 댓글들 출력
tmp[tmp.str.contains(&#39;[a-zA-Z]&#39;)]</code></pre>
<p>영어가 들어있는 댓글들의 내용을 확인 후 대부분의 영단어가 큰 의미를 가지고 있지 않다고 판단했다. 몇몇 글자들을 제외한 영어 글자를 삭제하였다.</p>
<pre><code class="language-python"># bb -&gt; 굿, zz -&gt; ㅋㅋ
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&quot;Good&quot;,&quot;굿&quot;)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&quot;bb+&quot;,&quot;굿&quot;)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&quot;zz+&quot;,&quot;ㅋㅋ&quot;)

# 나머지 영어 삭제
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].replace(&#39;[a-zA-Z]&#39;,&#39;&#39;,regex=True)</code></pre>
<h2 id="이모티콘">이모티콘</h2>
<p>이모티콘의 경우, 종류는 다양하지만 같은 의미를 담고 있는 것들을 묶어 한글로 대체하였다. &#39;❤️&#39;는 한글로 대체하기 애매하다고 생각하여 하나의 이모티콘으로 통일시켰다.</p>
<pre><code class="language-python"># 이모티콘 대체
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;[💟♡♥️❤❤️💓💕💖💗💘💙💚💛💜💝💞😍😘😻🤍🤎🥰🧡😚💋]+&#39;,&#39;❤️&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;[🙃🤣😆😀😊😄🤭😁😂]+&#39;,&#39;ㅋㅋ&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;[😢😭🥺]+&#39;,&#39;ㅠㅠ&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;[🔥👊💪]+&#39;,&#39;파이팅&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;[👍🏻👍]+&#39;,&#39;굿&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;[🎊🎉✨👏🥳💐]+&#39;,&#39;축하&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;[☆★⭐]+&#39;,&#39;별&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;[🍪]+&#39;,&#39;쿠키&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;[🙏]+&#39;,&quot;제발&quot;,regex=True)</code></pre>
<h2 id="자음">자음</h2>
<p>자음은 대부분 의미를 가지고 있지만, 후의 토큰화 과정에서는 그 의미를 캐치하지 못했다. 따라서 직접 의미를 풀어서 대체하였다.</p>
<pre><code class="language-python"># 자음 정리
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㄷㄷ+&#39;,&#39;덜덜&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅎㅇㅌ&#39;,&#39;파이팅&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅇㅈ&#39;,&#39;인정&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㄹㅈ&#39;,&#39;인정&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㄹㅈㄷ&#39;,&#39;레전드&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㄱㅇㅇ&#39;,&#39;귀여워&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㄷㄱ&#39;,&#39;두근&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅊㅎ&#39;,&#39;축하&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅊㅊ&#39;,&#39;축하&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅆㄹㄱ&#39;,&#39;쓰레기&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅁㄹ&#39;,&#39;몰라&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㄱㅊ&#39;,&#39;괜찮&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅁㅊ&#39;,&#39;미친&#39;,regex=True)

# ㅎㅎ, ㅋㅋ 는 댓글마다 그 글자수가 달라 통일시킴.
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅎㅎ+&#39;,&#39;ㅎㅎ&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅋㅋ+&#39;,&#39;ㅋㅋ&#39;,regex=True)</code></pre>
<h2 id="모음">모음</h2>
<p>모음의 경우 &#39;ㅜㅜ&#39;와 &#39;ㅠㅠ&#39; 외에는 대부분 오타로 보였다. 따라서 글자수만 통일시켜 주었다.</p>
<pre><code class="language-python"># 모음 정리
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅜ+&#39;,&#39;ㅠㅠ&#39;,regex=True)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅠㅠ+&#39;,&#39;ㅠㅠ&#39;,regex=True)</code></pre>
<h2 id="최종정리">최종정리</h2>
<pre><code class="language-python"># 한국어, 숫자, 띄어쓰기, 하트 제외 삭제
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;[^0-9ㅋㅎㅠ가-힣❤️ ]&#39;,&#39;&#39;,regex=True)

# 자음과 모음 앞뒤에 띄어쓰기를 넣어 별개의 단어로 판단하도록 함.
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅋㅋ&#39;,&#39; ㅋㅋ &#39;)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅎㅎ&#39;,&#39; ㅎㅎ &#39;)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39;ㅠㅠ&#39;,&#39; ㅠㅠ &#39;)
commentData[&#39;comment&#39;] = commentData[&#39;comment&#39;].str.replace(&#39; +&#39;,&#39; &#39;, regex=True)

## 의미가 없는 댓글 삭제
noMean = []
for i in range(len(commentData)):
    if commentData.loc[i,&#39;comment&#39;] == &#39;&#39;: noMean.append(i)
    elif commentData.loc[i,&#39;comment&#39;] == &#39; &#39;: noMean.append(i)
commentData.drop(noMean, inplace=True)
commentData.reset_index(drop=True, inplace=True)</code></pre>
<h1 id="✂-토큰화">✂ 토큰화</h1>
<h2 id="한셀-교정">한셀 교정</h2>
<p>댓글의 경우, 맞춤법이나 띄어쓰기가 정확하게 갖추어지지 않기 때문에 토큰화 전에 교정 과정이 필요했다. 한국어 교정을 해주는 &#39;hanspell&#39;  패키지를 이용하였다.</p>
<pre><code class="language-python"># 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,&#39;comment&#39;] = spell_checker.check(commentData.loc[i,&#39;comment&#39;]).checked</code></pre>
<h2 id="형태소-분석기-비교">형태소 분석기 비교</h2>
<p>konlpy 의 okt, kkma, komoran, mecab 을 이용해보았다. <span style="color:red">mecab의 경우 윈도우에서 지원하지 않기 때문에 코랩에 설치 후 확인하였다.</span></p>
<pre><code class="language-python"># 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
## -&gt; 2022년 11월에 코드 실행함. 2023년 4월에 확인 결과 오류가 뜨므로 수정 필요함.

# 원래 문장
original_sentence = commentData.loc[20,&#39;comment&#39;]
print(original_sentence) 
## &#39;벌레는 진짜아니지이그래도 짜파게티에 계란과 군만두는 뭐너무 맛있어보이잖아요오나 한입만❤️&#39;

# 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 -&gt; 코드 진행 시간이 너무 오래 걸려 제외함.
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)
## &#39;벌레는 진짜 아니지 이 그래도 짜파게티에 계란과 군만두는 뭐 너무 맛있어 보이잖아요 오나 한입만❤️&#39;

# 교정 후 Okt
print(*okt.morphs(spelled_sentence, stem=True))
## 벌레 는 진짜 아니다 이 그래도 짜파게티 에 계란 과 군 만두 는 뭐 너무 맛있다 보이다 오 나 한 입 만 ❤️

# 교정 후 Kkma
print(*kkma.morphs(spelled_sentence))
## 벌레 는 진짜 아니 지 이 그리하 여도 짜 아 파 게 티 에 계란 과 군만두 는 뭐 너무 맛있 어 보이 잖아요 오 나 한입 만 ❤️

# 교정 후 Mecab
print(*kkma.morphs(spelled_sentence))
## 벌레는 진짜 아니지 이 그래도 짜파게티에 계란과 군만두는 뭐 너무 맛있어 보이잖아요 오나 한입만❤️</code></pre>
<table>
<thead>
<tr>
<th align="center">형태소 분석기</th>
<th align="left">출력 결과</th>
</tr>
</thead>
<tbody><tr>
<td align="center">원래 문장</td>
<td align="left">벌레는 진짜아니지이그래도 짜파게티에 계란과 군만두는 뭐너무 맛있어보이잖아요오나 한입만❤️</td>
</tr>
<tr>
<td align="center">haspell 교정</td>
<td align="left">벌레는 진짜 아니지 이 그래도 짜파게티에 계란과 군만두는 뭐 너무 맛있어 보이잖아요 오나 한입만❤️</td>
</tr>
<tr>
<td align="center">Okt</td>
<td align="left">벌레 는 진짜 아니다 이 그래도 짜파게티 에 계란 과 군 만두 는 뭐 너무 맛있다 보이다 오 나 한 입 만 ❤️</td>
</tr>
<tr>
<td align="center">Kkma</td>
<td align="left">벌레 는 진짜 아니 지 이 그리하 여도 짜 아 파 게 티 에 계란 과 군만두 는 뭐 너무 맛있 어 보이 잖아요 오 나 한입 만 ❤️</td>
</tr>
<tr>
<td align="center">Mecab</td>
<td align="left">벌레는 진짜 아니지 이 그래도 짜파게티에 계란과 군만두는 뭐 너무 맛있어 보이잖아요 오나 한입만❤️</td>
</tr>
</tbody></table>
<p>위 형태소 분석기 중 mecab이 가장 효율적이라고 판단하였다.</p>
<h2 id="mecab-형태소-분석-토큰화">Mecab 형태소 분석, 토큰화</h2>
<p>Mecab을 이용하여 형태소별로 문장을 쪼갰다. 각 형태소 중 조사, 어미 등 의미없는 단어는 제외하며 토큰화를 진행하였다. 정식연재와 비정식연재의 단어들을 워드 클라우드 형태로 시각화하였다.</p>
<pre><code class="language-python"># 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 = [&#39;NNG&#39;,&#39;NNP&#39;,&#39;NNBC&#39;,&#39;NR&#39;,&#39;NP&#39;,&#39;VV&#39;,&#39;VA&#39;,&#39;MAG&#39;,&#39;IC&#39;,&#39;XPN&#39;,&#39;XR&#39;]

# 정식연재 토큰화
pubCom = commentData[commentData[&#39;isPublic&#39;] == 1]
public_mecab = []
for sentence in pubCom[&#39;comment&#39;]:
  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 [&#39;VV&#39;,&#39;VA&#39;]: # 동사와/형용사 + 어미
      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=&#39;/usr/share/fonts/truetype/nanum/NanumGothic.ttf&#39;, background_color=&#39;white&#39;)
gen = wc.generate_from_frequencies(wordDict)
plt.figure()
plt.imshow(gen)
plt.axis(&#39;off&#39;)

# 비정식연재 토큰화
notPubCom = commentData[commentData[&#39;isPublic&#39;] == 0]
notPublic_mecab = []
for sentence in notPubCom[&#39;comment&#39;]:
  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 [&#39;VV&#39;,&#39;VA&#39;]:
      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=&#39;/usr/share/fonts/truetype/nanum/NanumGothic.ttf&#39;, background_color=&#39;white&#39;)
gen = wc.generate_from_frequencies(wordDict2)
plt.figure()
plt.imshow(gen)
plt.axis(&#39;off&#39;)</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/c0d7d213-3f40-432b-99c2-af97130f5e7a/image.png" alt=""></p>
<p>정식연재와 비정식연재 웹툰의 순위권 단어들이 비슷하게 나온다. 따라서 비율을 고려해 보았다.</p>
<h1 id="🏅-단어-선택-및-변수화">🏅 단어 선택 및 변수화</h1>
<pre><code class="language-python"># 전체 댓글 토큰화
commentData2 = commentData.copy()
for i in range(len(commentData)):
  sentence = commentData.loc[i,&#39;comment&#39;]
  if type(sentence) == float: continue

  tokenized_sentence = mecab.pos(sentence)
  token = &#39;&#39;
  for j in range(len(tokenized_sentence)):
    if tokenized_sentence[j][1] in goodPos:
      token += &#39; &#39;+tokenized_sentence[j][0]
    elif tokenized_sentence[j][1][:2] in [&#39;VV&#39;,&#39;VA&#39;]: # 체언접두사와 어근
      token += &#39; &#39;+tokenized_sentence[j][0]
  commentData2.loc[i,&#39;comment&#39;] = token

# 정식과 비정식을 나눌 수 있다고 판단되는 들
wordsList = [&#39;가&#39;, &#39;가셨으면&#39;, &#39;가즈아&#39;, &#39;감동&#39;, &#39;감성&#39;, &#39;감정&#39;, &#39;갑시다&#39;, &#39;개성&#39;, &#39;계속&#39;, &#39;고침&#39;, &#39;고퀄&#39;, &#39;공감&#39;, &#39;괜찮&#39;, &#39;굿&#39;, &#39;궁금&#39;, &#39;귀여&#39;, &#39;귀여우&#39;, &#39;귀여운&#39;, &#39;귀여움&#39;, &#39;귀여워&#39;, &#39;귀여워서&#39;, &#39;귀염&#39;, &#39;귀엽&#39;, &#39;그리&#39;, &#39;그림&#39;, &#39;기다렸&#39;, &#39;기다리&#39;, &#39;기대&#39;, &#39;깜찍&#39;, &#39;꾸준히&#39;, &#39;나쁜&#39;, &#39;네이버&#39;, &#39;다음&#39;, &#39;담당자&#39;, &#39;답답&#39;, &#39;대박&#39;, &#39;대작&#39;, &#39;더&#39;, &#39;덜&#39;, &#39;데려가&#39;, &#39;독특&#39;, &#39;두근두근&#39;, &#39;드디어&#39;, &#39;등록&#39;, &#39;디테일&#39;, &#39;따뜻&#39;, &#39;매력&#39;, &#39;매주&#39;, &#39;명작&#39;, &#39;모셔&#39;, &#39;몰입&#39;, &#39;무서워&#39;, &#39;무섭&#39;, &#39;미쳤&#39;, &#39;미친&#39;, &#39;반갑&#39;, &#39;발암&#39;, &#39;베스트&#39;, &#39;별로&#39;, &#39;부들부들&#39;, &#39;분량&#39;, &#39;분위기&#39;, &#39;비슷&#39;, &#39;빨리&#39;, &#39;사이다&#39;, &#39;새로&#39;, &#39;새로운&#39;, &#39;색감&#39;, &#39;생각&#39;, &#39;생각나&#39;, &#39;설레&#39;, &#39;세계관&#39;, &#39;소름&#39;, &#39;소원&#39;, &#39;소재&#39;, &#39;스타일&#39;, &#39;스토리&#39;, &#39;승격&#39;, &#39;시키&#39;, &#39;신기&#39;, &#39;신선&#39;, &#39;실화&#39;, &#39;싫&#39;, &#39;아쉽&#39;, &#39;알림&#39;, &#39;어서&#39;, &#39;얼른&#39;, &#39;연재&#39;, &#39;연출&#39;, &#39;열심히&#39;, &#39;옆&#39;, &#39;예뻐요&#39;, &#39;예쁘&#39;, &#39;예쁜&#39;, &#39;오랜만&#39;, &#39;오지&#39;, &#39;올라가&#39;, &#39;올려&#39;, &#39;올리&#39;, &#39;완결&#39;, &#39;웃&#39;, &#39;웃겨&#39;, &#39;웃겨요&#39;, &#39;웃기&#39;, &#39;원합니다&#39;, &#39;위&#39;, &#39;응원&#39;, &#39;이뻐요&#39;, &#39;이쁘&#39;, &#39;이상&#39;, &#39;이야기&#39;, &#39;작품&#39;, &#39;작화&#39;, &#39;장면&#39;, &#39;재미&#39;, &#39;재미나&#39;, &#39;재미있&#39;, &#39;재밌&#39;, &#39;잼&#39;, &#39;전개&#39;, &#39;정식&#39;, &#39;좋&#39;, &#39;좋아하&#39;, &#39;주인공&#39;, &#39;주제&#39;, &#39;진심&#39;, &#39;짧&#39;, &#39;쩔&#39;, &#39;참신&#39;, &#39;처음&#39;, &#39;최강&#39;, &#39;최고&#39;, &#39;추천&#39;, &#39;축하&#39;, &#39;취향&#39;, &#39;친구&#39;, &#39;캐릭터&#39;, &#39;쿠키&#39;, &#39;퀄리티&#39;, &#39;탄탄&#39;, &#39;파이팅&#39;, &#39;팬&#39;, &#39;표절&#39;, &#39;표정&#39;, &#39;피드백&#39;, &#39;헉&#39;, &#39;현기증&#39;, &#39;현실&#39;, &#39;흑흑&#39;, &#39;흥미&#39;, &#39;흥미진진&#39;, &#39;힐링&#39;, &#39;힘내&#39;]

# 같은 의미의 단어들 하나로 통일
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;가셨으면&#39;,&#39;가&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;갑시다&#39;,&#39;가&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;귀여우&#39;,&#39;귀엽&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;귀여운&#39;,&#39;귀엽&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;귀여움&#39;,&#39;귀엽&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;귀여워서&#39;,&#39;귀엽&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;귀염&#39;,&#39;귀엽&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;귀여워&#39;,&#39;귀엽&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;귀여&#39;,&#39;귀엽&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;그리&#39;,&#39;그림&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;기다렸&#39;,&#39;기다리&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;무서워&#39;,&#39;무섭&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;미친&#39;,&#39;미쳤&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;예뻐요&#39;,&#39;예쁘&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;예쁜&#39;,&#39;예쁘&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;올라가&#39;,&#39;올려&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;올리&#39;,&#39;올려&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;웃&#39;,&#39;웃겨&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;웃겨요&#39;,&#39;웃겨&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;웃기&#39;,&#39;웃겨&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;이뻐요&#39;,&#39;예쁘&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;이쁘&#39;,&#39;예쁘&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;재미나&#39;,&#39;재미&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;재미있&#39;,&#39;재미&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;재밌&#39;,&#39;재미&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;잼&#39;,&#39;재미&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;좋&#39;,&#39;좋아하&#39;)
commentData2[&#39;comment&#39;] = commentData2[&#39;comment&#39;].str.replace(&#39;흥미진진&#39;,&#39;흥미&#39;)

# 한 댓글 내에 중복등장하는 단어 정리
for i in range(len(commentData2)):
    comSet = set(commentData2.loc[i,&#39;comment&#39;].split(&#39; &#39;))
    token = &#39;&#39;
    for j in range(1,len(comSet)):
        token += &#39; &#39;+list(comSet)[j]
    commentData2.loc[i,&#39;comment&#39;] = token

# 정식연재와 비정식연재 웹툰의 단어들 빈도 비율 비교
token_ratio = pd.DataFrame(arr, columns=[&#39;public&#39;,&#39;notPublic&#39;])
for i in range(len(commentData2)):
    tokenList = commentData2.loc[i,&#39;comment&#39;].split(&#39; &#39;)[1:]
    for word in token_ratio.index:
        if word in tokenList:
            if commentData2.loc[i,&#39;isPublic&#39;] == 1:
                token_ratio.loc[word,&#39;public&#39;] += 1/(len(pubId)*6)
            elif commentData.loc[i,&#39;isPublic&#39;] == 0:
                token_ratio.loc[word,&#39;notPublic&#39;] += 1/(len(notPubId)*6)

# 정식연재가 2배 많은 단어
words1 = token_ratio[token_ratio[&#39;public&#39;] &gt; token_ratio[&#39;notPublic&#39;] * 2]
print(words1.index)
## &#39;고침&#39;, &#39;나쁜&#39;, &#39;담당자&#39;, &#39;덜&#39;, &#39;데려가&#39;, &#39;두근두근&#39;, &#39;등록&#39;, &#39;디테일&#39;, &#39;매주&#39;, &#39;모셔&#39;, &#39;무섭&#39;, &#39;미쳤&#39;, &#39;발암&#39;, &#39;부들부들&#39;, &#39;사이다&#39;, &#39;새로&#39;, &#39;색감&#39;, &#39;설레&#39;, &#39;소름&#39;, &#39;소원&#39;, &#39;시키&#39;, &#39;얼른&#39;, &#39;옆&#39;, &#39;오지&#39;, &#39;위&#39;, &#39;작화&#39;, &#39;전개&#39;, &#39;쩔&#39;, &#39;최강&#39;, &#39;친구&#39;, &#39;쿠키&#39;, &#39;탄탄&#39;, &#39;표절&#39;, &#39;피드백&#39;, &#39;헤어지&#39;, &#39;현기증&#39;, &#39;현실&#39;

# 비정식연재가 2배 많은 단어
words0 = token_ratio[token_ratio[&#39;public&#39;] * 2 &lt; token_ratio[&#39;notPublic&#39;]]
print(words0.index)
## &#39;감동&#39;, &#39;감성&#39;, &#39;감정&#39;, &#39;개성&#39;, &#39;깜찍&#39;, &#39;명작&#39;, &#39;비슷&#39;, &#39;새로운&#39;, &#39;소식&#39;, &#39;신기&#39;, &#39;아쉽&#39;, &#39;오랜만&#39;, &#39;완결&#39;, &#39;원합니다&#39;, &#39;이야기&#39;, &#39;장면&#39;, &#39;짧&#39;, &#39;추천&#39;, &#39;축하&#39;</code></pre>
<p>최종 단어 선택! 위의 각 단어들 중 더더욱 정식연재 여부를 구별할 수 있는 단어들을 선택하였다.</p>
<blockquote>
<ul>
<li>정식 연재 단어 words1
&#39;나쁜&#39;, &#39;데려가&#39;, &#39;두근두근&#39;, &#39;등록&#39;, &#39;디테일&#39;, &#39;매주&#39;, &#39;무섭&#39;, &#39;미쳤&#39;, &#39;발암&#39;, &#39;부들부들&#39;, &#39;사이다&#39;, &#39;색감&#39;, &#39;소름&#39;, &#39;얼른&#39;, &#39;옆&#39;, &#39;오지&#39;, &#39;작화&#39;, &#39;전개&#39;, &#39;쩔&#39;, &#39;쿠키&#39;, &#39;탄탄&#39;, &#39;현실&#39;</li>
</ul>
</blockquote>
<blockquote>
<ul>
<li>비정식 연재 단어 words0
&#39;감동&#39;, &#39;명작&#39;, &#39;비슷&#39;, &#39;새로운&#39;, &#39;소식&#39;, &#39;아쉽&#39;, &#39;오랜만&#39;, &#39;완결&#39;, &#39;이야기&#39;, &#39;짧&#39;, &#39;축하&#39;</li>
</ul>
</blockquote>
<p>이제, 각 웹툰별로 정식연재와 비정연재 단어들이 몇번 나타나는지를 카운트하여 그 숫자를 값으로 넣었다.</p>
<pre><code class="language-python">titleId = commentData2.drop_duplicates(&#39;titleId&#39;)[&#39;titleId&#39;]
commentData3 = pd.DataFrame(columns= [&#39;titleId&#39;,&#39;words0&#39;,&#39;words1&#39;])
commentData3[&#39;titleId&#39;] = titleId
commentData3.reset_index(drop=True, inplace=True)

for i in range(len(commentData3)):
  comment_dt = commentData2[&#39;comment&#39;][commentData2[&#39;titleId&#39;] == commentData3.loc[i,&#39;titleId&#39;]]
  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,&#39;words0&#39;] = w0
  commentData3.loc[i,&#39;words1&#39;] = w1

commentData3.to_csv(&#39;comment_words.csv&#39;, index=False)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[월간 데이콘 기계 고장 진단 AI 경진대회]]></title>
            <link>https://velog.io/@goodjin_55/%EC%9B%94%EA%B0%84-%EB%8D%B0%EC%9D%B4%EC%BD%98-%EA%B8%B0%EA%B3%84-%EA%B3%A0%EC%9E%A5-%EC%A7%84%EB%8B%A8-AI-%EA%B2%BD%EC%A7%84%EB%8C%80%ED%9A%8C</link>
            <guid>https://velog.io/@goodjin_55/%EC%9B%94%EA%B0%84-%EB%8D%B0%EC%9D%B4%EC%BD%98-%EA%B8%B0%EA%B3%84-%EA%B3%A0%EC%9E%A5-%EC%A7%84%EB%8B%A8-AI-%EA%B2%BD%EC%A7%84%EB%8C%80%ED%9A%8C</guid>
            <pubDate>Sun, 12 Feb 2023 15:02:02 GMT</pubDate>
            <description><![CDATA[<p>데이콘에서 음향 데이터를 이용해 기계의 고장 여부를 판단하는 대회가 열렸다. 한번도 다뤄본 적 없는 음향데이터를 만져볼 좋은 기회라고 생각되어 대회에 참가하였다.</p>
<p><a href="https://dacon.io/competitions/official/236036/overview/description">https://dacon.io/competitions/official/236036/overview/description</a></p>
<h1 id="데이터-살펴보기">데이터 살펴보기</h1>
<p>train.csv 에는 아래 사진과 같이 샘플의 고유ID, 음향 파일의 경로, FAN의 종류(0,2), 기계 고장 여부(0:정상, 1:고장)로 구성되어 있다. 이때, <span style="color:lightred">train의 LABEL은 모두 정상 샘플만 존재한다</span>. train 폴더에는 SAMPLE_PATH 에 나타나는 소리파일(.wav)이 있다.</p>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/2034ef51-f1f1-49bf-a841-1cf1ee751437/image.png" alt=""></p>
<h1 id="패키지-import">패키지 import</h1>
<pre><code class="language-python">import numpy as np
import pandas as pd
from tqdm.auto import tqdm
import warnings
warnings.filterwarnings(action=&#39;ignore&#39;)

import librosa
import librosa.display
import matplotlib.pyplot as plt

from sklearn.svm import OneClassSVM</code></pre>
<h1 id="librosa를-이용한-시각화">librosa를 이용한 시각화</h1>
<p>SAMPLE_ID 가 &#39;TRAIN_0000&#39;인 샘플을 load하여 시각화해 보았다. 파라미터 sr을 이용해 sampling rate(주파수 분석의 시간 간격)을 조정할 수 있다. 기본값은 22050 이다.</p>
<pre><code class="language-python">trainData = pd.read_csv(&#39;./train.csv&#39;)
train_path = trainData[&#39;SAMPLE_PATH&#39;][0]
train_y, sr = librosa.load(train_path)
plt.plot(train_y)</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/3e5f757b-f8b2-4a16-a422-d3f28ae35ad2/image.png" alt=""></p>
<h3 id="short-time-fourier-transform-stft">Short Time Fourier Transform (STFT)</h3>
<p>시간 단위로 짧게 쪼개서 푸리에 변환을 하는 방법이다.</p>
<pre><code class="language-python">train_stft = np.abs(librosa.stft(train_y))
librosa.display.specshow(librosa.amplitude_to_db(train_stft, ref= np.mean), 
                         y_axis=&#39;log&#39;, sr=sr, x_axis=&#39;time&#39;)
plt.colorbar(format=&#39;%+2.0f dB&#39;)
plt.title(&#39;STFT _train&#39;)
plt.tight_layout()
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/3ece8cc1-8646-4436-b13c-918f6aba59d9/image.png" alt=""></p>
<h3 id="mel-spectrogram">Mel Spectrogram</h3>
<p>Mel Scale 변환을 통해 오디오를 분석하여 특징을 추출하는 방법이다.</p>
<pre><code class="language-python">train_mel = librosa.feature.melspectrogram(y= train_y, sr= sr)
train_mel_dB = librosa.amplitude_to_db(train_mel, ref= np.mean)
librosa.display.specshow(train_mel_dB, y_axis=&#39;mel&#39;, sr=sr, x_axis=&#39;time&#39;)
plt.colorbar(format=&#39;%+2.0f dB&#39;)
plt.title(&#39;Mel-Spectrogram _train&#39;)
plt.tight_layout()
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/ee5d8591-9085-423f-9f63-56cd842ba42e/image.png" alt=""></p>
<h3 id="mel-frequency-cepstral-coefficients-mfcc">Mel-Frequency Cepstral Coefficients (MFCC)</h3>
<p>사람의 청각구조를 반영하여 음성 정보를 추출하는 방법으로, mel-spectrum에서 Cepstral 분석으로 추출한 값이다. Cepstral 분석이란, 스펙트럼 신호의 로그값에 역푸리에 변환을 하는 것을 말한다.</p>
<pre><code class="language-python">train_mfccs = librosa.feature.mfcc(train_y, sr= sr, n_mfcc=128)
librosa.display.specshow(train_mfccs, sr=sr, x_axis=&#39;time&#39;)
plt.colorbar()
plt.title(&#39;MFCC _train&#39;)
plt.tight_layout()
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/073ec531-5cdb-4ed0-b067-7ae39912010a/image.png" alt=""></p>
<h3 id="chroma-frequencies">Chroma Frequencies</h3>
<p>인간의 청각이 한 옥타브를 유사음으로 인지한다는 것에 기반하여, 모든 스펙트럼을 12개의 Bin으로 표현한다.</p>
<pre><code class="language-python">train_chromagram = librosa.feature.chroma_stft(train_y, sr= sr)
train_chromagram.shape # (12, 431)
librosa.display.specshow(train_chromagram, x_axis=&#39;time&#39;, y_axis= &#39;chroma&#39;)
plt.colorbar()
plt.title(&#39;Chroma _train&#39;)
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/6217df2d-ddb5-4447-b206-92224bf90945/image.png" alt=""></p>
<h3 id="zero-crossing-rate">Zero Crossing Rate</h3>
<p>음파가 양에서 음, 음에서 양으로 바뀌는 비율이다. 0을 많이 지날수록 노이즈가 많음을 뜻한다.</p>
<pre><code class="language-python">train_zero_crossings = librosa.zero_crossings(train_y, pad=False)
sum(train_zero_crossings) # 결과는 21960</code></pre>
<h1 id="데이터-전처리">데이터 전처리</h1>
<p>위에서 시도했던 여러 방법들 중 mfcc를 선택하고, 파라미터 n_mfcc = 27로 설정하여 소리의 특징을 추출하였다. </p>
<pre><code class="language-python">train_df = pd.read_csv(&#39;train.csv&#39;)
test_df = pd.read_csv(&#39;test.csv&#39;)

def get_mfcc_feature(df, num):
    features = []
    for path in tqdm(df[&#39;SAMPLE_PATH&#39;]):
        y, sr = librosa.load(path, sr= 16000)

        mfcc = librosa.feature.mfcc(y= y, sr= sr, n_mfcc= num)

        y_feature = []
        for e in mfcc:
            y_feature.append(np.mean(e))
        features.append(y_feature)
    return features

train_features = get_mfcc_feature(train_df, 27)
test_features = get_mfcc_feature(test_df, 27)</code></pre>
<h1 id="모델-적합">모델 적합</h1>
<p>전처리를 통해 얻은 특징의 수치들을 데이터프레임으로 저장하여, 각 특징별 상관관계를 구하여 변수를 선택하는 과정을 추가하였다. 그 중 Col5, Col6, Col7 을 제외하였을 때 점수가 높게 나왔다.</p>
<p>모델로는 OCSVM을 선택하였다. 비지도학습의 머신러닝 모델로는 Isolation Forest, OneClassSVM, LocalOutlierFactor 등이 있다. 주어진 데이터들을 바탕으로 이 데이터에서 많이 떨어져 있는 데이터를 이상치로 판단하는 모델들이다. 그 중 OCSVM은 n차원의 좌표축에서 데이터와 원점 사이의 거리를 기준으로 이상치를 판단하는 방법이다. 파라미터는 kernel=&#39;rbf&#39;, gamma=0.004, nu=0.03 으로 설정하였다.</p>
<pre><code class="language-python"># 데이터 프레임으로 변환
train_features_df = pd.DataFrame(train_features, columns = [f&#39;Col{i}&#39; for i in range(1,28)])
test_features_df = pd.DataFrame(test_features, columns = [f&#39;Col{i}&#39; for i in range(1,28)])

# corr 구한 후, 0.7 이상의 값들만 확인
cor = train_features_df.corr()
pd.set_option(&#39;display.max_rows&#39;,None)
pd.set_option(&#39;display.max_columns&#39;,None)
cor[abs(cor) &gt; 0.7]

# 변수 선택
train_features_df = pd.DataFrame(train_features, columns = [f&#39;Col{n}&#39; for n in range(1,28)])
train_features_df.drop([&#39;Col6&#39;,&#39;Col5&#39;,&#39;Col7&#39;], axis=1, inplace=True)
test_features_df = pd.DataFrame(test_features, columns = [f&#39;Col{n}&#39; for n in range(1,28)])
test_features_df.drop([&#39;Col6&#39;,&#39;Col5&#39;,&#39;Col7&#39;], axis=1, inplace=True)

# OCSVM 훈련
svm = OneClassSVM(kernel=&#39;rbf&#39;, gamma= 0.004, nu=0.03)
svm.fit(train_features_df)</code></pre>
<h1 id="테스트-데이터-예측">테스트 데이터 예측</h1>
<p>test 데이터를 예측하여 1(정상)과 -1(불량)로 표현된 값들을 0(정상)과 1(불량)로 바꿔주고, 파일로 저장하여 제출하였다.</p>
<pre><code class="language-python"># 0, 1로 변환
def get_pred_label(model_pred):
    # 1: 정상, -1: 불량  =&gt; 0: 정상, 1:불량 변환
    model_pred = np.where(model_pred == 1, 0, model_pred)
    model_pred = np.where(model_pred == -1, 1, model_pred)
    return model_pred

# 예측
test_pred = svm.predict(test_features_df)
test_pred = get_pred_label(test_pred)

# 제출
submit = pd.read_csv(&#39;./answer/sample_submission.csv&#39;)
submit[&#39;LABEL&#39;] = test_pred
submit.to_csv(&#39;./answer/ans1.csv&#39;, index= False)</code></pre>
<h1 id="후기">후기</h1>
<p>모델을 선택하는 과정에서 deep SVDD 를 이용해보고 싶었다. 딥러닝까지 시도하기에는 시간이 부족하여 모델링은 하지 못했지만, 더 공부하여 딥러닝을 이용한 모델링도 도전해볼 것이다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[베스트도전 웹툰의 정식연재 승격 확률 예측 - 3. 데이터 변수화]]></title>
            <link>https://velog.io/@goodjin_55/%EB%B2%A0%EC%8A%A4%ED%8A%B8%EB%8F%84%EC%A0%84-%EC%9B%B9%ED%88%B0%EC%9D%98-%EC%A0%95%EC%8B%9D%EC%97%B0%EC%9E%AC-%EC%8A%B9%EA%B2%A9-%ED%99%95%EB%A5%A0-%EC%98%88%EC%B8%A1-3.-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%80%EC%88%98%ED%99%94</link>
            <guid>https://velog.io/@goodjin_55/%EB%B2%A0%EC%8A%A4%ED%8A%B8%EB%8F%84%EC%A0%84-%EC%9B%B9%ED%88%B0%EC%9D%98-%EC%A0%95%EC%8B%9D%EC%97%B0%EC%9E%AC-%EC%8A%B9%EA%B2%A9-%ED%99%95%EB%A5%A0-%EC%98%88%EC%B8%A1-3.-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%80%EC%88%98%ED%99%94</guid>
            <pubDate>Mon, 06 Feb 2023 13:03:01 GMT</pubDate>
            <description><![CDATA[<p>이전에 수집한 데이터들을 &#39;mywebtoon_data.csv&#39; 와 &#39;mycomment_data.csv&#39;에 저장하였다.</p>
<h1 id="🔎-수집한-데이터">🔎 수집한 데이터</h1>
<p>아래는 <strong>&#39;mywebtoon_data.csv&#39;</strong> 의 columns 정보이다.</p>
<blockquote>
<ul>
<li>titleId : 각 웹툰의 고유번호</li>
</ul>
</blockquote>
<ul>
<li>isPublic : 정식연재 승격 여부</li>
<li>totalStar : 전체별점</li>
<li>heart : 하트수</li>
<li>contentGenre : 내용장르 10개 중 n개
<span style="color:darkred">daily/comic/fantasy/action/drama/pure/sensibility/thrill/historical/sports</span></li>
<li>typeGenre : 형식장르 3개 중 1개
<span style="color:darkred">에피소드/옴니버스/스토리</span></li>
<li>star<strong>(i)</strong> : i번째 회차의 별점 *(i = 1,2,3,-3,-2,-1)</li>
<li>starPar<strong>(i)</strong> : i번째 회차의 별점참여수</li>
<li>day<strong>(i)</strong> : i번째 회차의 등록일</li>
<li>views<strong>(i)</strong> : i번째 회차의 조회수</li>
</ul>
<p>아래는 <strong>&#39;mycomment_data.csv&#39;</strong> 의 columns 정보이다.</p>
<blockquote>
<ul>
<li>titleId : 각 웹툰의 고유번호</li>
</ul>
</blockquote>
<ul>
<li>isPublic : 정식연재 승격 여부</li>
<li>comment : 댓글 내용</li>
<li>like : 해당 댓글의 좋아요 수</li>
<li>hate : 해당 댓글의 싫어요 수</li>
</ul>
<h1 id="📄-데이터-정리">📄 데이터 정리</h1>
<h3 id="연재-시작-날짜로-웹툰-선택">연재 시작 날짜로 웹툰 선택</h3>
<p>지금까지는 모든 웹툰을 수집하였다. 급격히 바뀌는 트렌드를 반영하여 너무 오래된 웹툰들은 분석 데이터에서 제외하자는 판
단을 하였다.</p>
<pre><code class="language-python">webtoon[&#39;day1&#39;] = pd.to_datetime(webtoon[&#39;day1&#39;])
webtoon[&#39;day1&#39;].dt.year.value_counts().plot.bar()</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/8dfbae0b-8069-4964-a27d-7fb59b2626aa/image.png" alt=""></p>
<p>2012년에 베스트도전 웹툰의 수가 급격히 늘었다. 따라서 2012년 전의 웹툰들은 삭제하였다.</p>
<pre><code class="language-python">webtoon = webtoon[webtoon[&#39;day1&#39;].dt.year &gt;= 2012]
webtoon.reset_index(drop= True, inplace= True)

# 댓글 데이터에서도 제외
titleId = webtoon[&#39;titleId&#39;].unique()
for id in comment[&#39;titleId&#39;]:
    if id not in titleId:
        comment.drop(comment[comment[&#39;titleId&#39;] == id].index, inplace=True)
comment.reset_index(drop= True, inplace= True)</code></pre>
<p>그 결과, 전체 웹툰의 수는 2623개, 공식연재(isPublic == 1) 웹툰의 수는 153개였다.</p>
<h3 id="4화-5화밖에-없는-웹툰-정리">4화, 5화밖에 없는 웹툰 정리</h3>
<p>회차의 등록일을 기준으로 4개 또는 5개의 회차만 존재하는 웹툰을 정리하였다. 예를 들어, 4개의 회차만 존재한다면, 데이터 수집 순서가 (-1) -&gt; (-2) -&gt; (-3) -&gt; 1 -&gt; 2 -&gt; 3 이므로, 2와 3은 비어있어야 한다. 5개의 회차만 존재한다면, 3이 비어있는 데이터여야 한다.</p>
<pre><code class="language-python"># 4화만 있는 웹툰 : 2,3 삭제
only4 = data[data[&#39;day2&#39;] == data[&#39;day(-2)&#39;]].index
data.loc[only4, [&#39;star2&#39;,&#39;starPar2&#39;,&#39;views2&#39;,&#39;day2&#39;]] = np.NaN
data.loc[only4, [&#39;star3&#39;,&#39;starPar3&#39;,&#39;views3&#39;,&#39;day3&#39;]] = np.NaN
only4 = data[data[&#39;day2&#39;].isnull()].index
# 5화만 있는 웹툰 : 3 삭제
only5 = data[data[&#39;day3&#39;] == data[&#39;day(-3)&#39;]].index
data.loc[only5, [&#39;star3&#39;,&#39;starPar3&#39;,&#39;views3&#39;,&#39;day3&#39;]] = np.NaN
only5 = data[data[&#39;day3&#39;].isnull()].index</code></pre>
<h1 id="📊-데이터-eda-및-변수화">📊 데이터 eda 및 변수화</h1>
<p>---고려해본 것!---</p>
<ol>
<li>초반 회차 데이터와 후반 회차 데이터를 하나의 변수로 나타내기</li>
<li>분포가 편향된 변수들에 로그변환, 역변환, Box-cox 변환 등 반영하기</li>
</ol>
<h3 id="패키지-import">패키지 import</h3>
<pre><code>import numpy as np
import pandas as pd
from scipy import stats
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings(action=&#39;ignore&#39;)
from datetime import datetime
import time

data = pd.read_csv(&#39;mywebtoon_data.csv&#39;)</code></pre><h3 id="종속변수-ispublic">종속변수 isPublic</h3>
<pre><code class="language-python">print(data[&#39;isPublic&#39;].value_counts())
plt.figure(figsize= (5,4))
data[&#39;isPublic&#39;].value_counts().plot(kind = &#39;bar&#39;)
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/26e89834-3aec-400b-a734-23c1d77af7ba/image.png" alt=""></p>
<p>정식연재 웹툰(isPublic == 1)이 매우 적으므로 (약 5%) 이를 고려하여 모델링에 참고하였다.</p>
<h3 id="totalstar">totalStar</h3>
<pre><code class="language-python">plt.figure(figsize= (6,3))
sns.distplot(data[&#39;totalStar&#39;])</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/924081a3-66cc-45d1-aeaf-084129a3bf97/image.png" alt=""></p>
<p>대부분의 별점이 9점대에 몰려있다. 이렇게 한쪽으로 몰려있는 데이터는 이후 모델링의 성능에 악영향을 끼칠 수 있다. 따라서 다양한 변환을 이용해 정규분포에 가까운 모양을 나타내거나 데이터를 분산시키는 방법을 선택하였다.</p>
<pre><code class="language-python">fig, ax = plt.subplots(ncols=2)
# 로그변환
sns.distplot(np.log(data[&#39;totalStar&#39;]), ax=ax[0], color=&#39;red&#39;)
# boxcox 변환
y, lambda_optimal = stats.boxcox(data[&#39;totalStar&#39;])
sns.distplot(y, ax=ax[1], color=&#39;red&#39;)</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/de2651e4-0ee9-49ab-aabf-032ac4917bf0/image.png" alt=""></p>
<p>왼쪽은 로그변환, 오른쪽은 Box-cox 변환 후 분포이다. totalStar 변수는 box-cox 변환이 데이터를 잘 분산시켰지만, 값이 너무 커지는 이유로 선택하지 않았다. 그러나 다른 변수들의 변환 및 변수화 결과, 값이 0 근처에서 그게 벗어나지 않아 각 데이터에 최솟값을 빼준 값을 새 데이터로 설정하였다.</p>
<h3 id="heart">heart</h3>
<pre><code class="language-python">data[&#39;heart&#39;] = data[&#39;heart&#39;].str.replace(&#39;,&#39;,&#39;&#39;) # 천단위 콤마 제거
data[&#39;heart&#39;] = data[&#39;heart&#39;].astype(int)

plt.figure(figsize= (6,3))
sns.distplot(data[&#39;heart&#39;])</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/6a4ad9fb-5dc9-441d-bbbd-61006bb44041/image.png" alt=""></p>
<p>로그변환!</p>
<pre><code class="language-python">sns.displot(np.log(data[&#39;heart&#39;]), color=&#39;red&#39;)</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/6ba0b57c-0bea-4120-9399-ffe0c26e90eb/image.png" alt=""></p>
<h3 id="star">star</h3>
<p>star1, star2, star3와 star(-3), star(-2), star(-1)의 평균으로 초반, 후반 별점을 구하여 분포를 확인하였다. 이때, 4개 또는 5개 회차밖에 존재하지 않는 웹툰은 존재하지 않는 데이터를 제외하고 평균을 구하였다.</p>
<pre><code class="language-python"># 3개 화의 평균을 이용해 초반, 후반으로 나눔.
data[&#39;starEarly&#39;] = (data[&#39;star1&#39;] + data[&#39;star2&#39;] + data[&#39;star3&#39;])/3
data[&#39;starLater&#39;] = (data[&#39;star(-1)&#39;] + data[&#39;star(-2)&#39;] + data[&#39;star(-3)&#39;])/3
data.loc[only5,&#39;starEarly&#39;] = (data[&#39;star1&#39;] + data[&#39;star2&#39;])/2
data.loc[only4,&#39;starEarly&#39;] = data[&#39;star1&#39;]

# 초반과 후반 분포
fig, ax = plt.subplots(ncols=2)
sns.distplot(data[&#39;starEarly&#39;], ax=ax[0])
sns.distplot(data[&#39;starLater&#39;], ax=ax[1])</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/bbd33acb-4a5c-4b96-ade8-b77eadcdcdf8/image.png" alt=""></p>
<p>변환 후 파생변수를 만들고자 하였으나, 변환 후 데이터의 분포에서 크게 효과를 보지 못하였다. 따라서 초반과 후반 별점의 차이(후반별점 - 초반별점) 또는 비율(후반별점 / 초반별점)을 구하여 파생변수로 만들었다.</p>
<pre><code class="language-python">data[&#39;starDif&#39;] = data[&#39;starLater&#39;] - data[&#39;starEarly&#39;] # 차이
data[&#39;starRatio&#39;] = data[&#39;starLater&#39;] / data[&#39;starEarly&#39;] # 비율

fig, ax = plt.subplots(ncols=2)
sns.distplot(data[&#39;starDif&#39;], ax=ax[0])
sns.distplot(data[&#39;starRatio&#39;], ax=ax[1])</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/7828c8e7-7824-49f8-8d18-b1d831fc3d1d/image.png" alt=""></p>
<p>왼쪽은 차이, 오른쪽은 비율의 분포 그래프이다. 둘 중 isPublic 변수와 상관관계가 높은 &#39;차이&#39; 를 이용하였다. (차이 : 0.061, 비율 : 0.055, <del>상관관계로 변수를 선택한 것이 옳은 방법인지는 더 공부해볼 것.</del>) </p>
<h3 id="starpar">starPar</h3>
<p>별점에서와 같은 방법으로, 초반 회차와 후반 회차의 별점참여수 평균의 로그변환 후, 비율을 파생변수로 만들었다.</p>
<pre><code class="language-python">data[&#39;starParEarly&#39;] = (data[&#39;starPar1&#39;] + data[&#39;starPar2&#39;] + data[&#39;starPar3&#39;])/3
data[&#39;starParLater&#39;] = (data[&#39;starPar(-1)&#39;] + data[&#39;starPar(-2)&#39;] + data[&#39;starPar(-3)&#39;])/3
data.loc[only5,&#39;starParEarly&#39;] = (data[&#39;starPar1&#39;] + data[&#39;starPar2&#39;])/2
data.loc[only4,&#39;starParEarly&#39;] = data[&#39;starPar1&#39;]

# 초반과 후반 분포
fig, ax = plt.subplots(ncols=2)
sns.distplot(data[&#39;starParEarly&#39;], ax=ax[0])
sns.distplot(data[&#39;starParLater&#39;], ax=ax[1])</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/54e1129d-8362-440a-978f-f559891fba99/image.png" alt=""></p>
<pre><code class="language-python"># 로그변환
fig, ax = plt.subplots(ncols=2)
sns.distplot(np.log(data[&#39;starParEarly&#39;]), ax=ax[0], color=&#39;red&#39;)
sns.distplot(np.log(data[&#39;starParLater&#39;]), ax=ax[1], color=&#39;red&#39;)</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/3443d3a9-5bbf-48b1-a39e-626ea80e9d03/image.png" alt=""></p>
<pre><code class="language-python"># 초반과 후반 로그변환의 비율 분포
data[&#39;starParRatio&#39;] = np.log(data[&#39;starParLater&#39;]) / np.log(data[&#39;starParEarly&#39;])
sns.distplot(data[&#39;starParRatio&#39;])</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/0b3b29b5-d19e-4981-bb63-b7fda10c2478/image.png" alt=""></p>
<h3 id="views">views</h3>
<p>마찬가지로, 초반 회차와 후반 회차의 조회수 평균의 로그변환 후, 비율을 파생변수로 만들었다.</p>
<pre><code class="language-python">data[&#39;viewsEarly&#39;] = (data[&#39;views1&#39;] + data[&#39;views2&#39;] + data[&#39;views3&#39;])/3
data[&#39;viewsLater&#39;] = (data[&#39;views(-1)&#39;] + data[&#39;views(-2)&#39;] + data[&#39;views(-3)&#39;])/3
data.loc[only5,&#39;viewsEarly&#39;] = (data[&#39;views1&#39;] + data[&#39;views2&#39;])/2
data.loc[only4,&#39;viewsEarly&#39;] = data[&#39;views1&#39;]

# 초반과 후반의 분포
fig, ax = plt.subplots(ncols=2)
sns.distplot(data[&#39;viewsEarly&#39;], ax=ax[0])
sns.distplot(data[&#39;viewsLater&#39;], ax=ax[1])</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/fc35d986-badc-4788-a9d5-b249de456f24/image.png" alt=""></p>
<pre><code class="language-python"># 로그변환
fig, ax = plt.subplots(ncols=2)
sns.distplot(np.log(data[&#39;viewsEarly&#39;]), ax=ax[0], color=&#39;red&#39;)
sns.distplot(np.log(data[&#39;viewsLater&#39;]), ax=ax[1], color=&#39;red&#39;)</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/302df1db-159c-4503-92fb-c1231f572dd4/image.png" alt=""></p>
<pre><code class="language-python"># 초반과 후반 로그변환의 비율 분포
data[&#39;viewsRatio&#39;] = np.log(data[&#39;viewsLater&#39;]) / np.log(data[&#39;viewsEarly&#39;])
sns.distplot(data[&#39;viewsRatio&#39;])</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/57332832-ac69-448b-a52e-64be3fb028d1/image.png" alt=""></p>
<h3 id="day">day</h3>
<p>회차별 등록일 데이터는 각 회차의 날짜의 차이를 계산하여, 얼마나 자주 연재하였나를 변수로 넣고자 하였다.</p>
<pre><code class="language-python"># 날짜 데이터로 변환
data[&#39;day1&#39;] = pd.to_datetime(data[&#39;day1&#39;])
data[&#39;day2&#39;] = pd.to_datetime(data[&#39;day2&#39;])
data[&#39;day3&#39;] = pd.to_datetime(data[&#39;day3&#39;])
data[&#39;day(-3)&#39;] = pd.to_datetime(data[&#39;day(-3)&#39;])
data[&#39;day(-2)&#39;] = pd.to_datetime(data[&#39;day(-2)&#39;])
data[&#39;day(-1)&#39;] = pd.to_datetime(data[&#39;day(-1)&#39;])

# 등록일 차이 계산
dateInterval1 = data[&#39;day2&#39;] - data[&#39;day1&#39;]
dateInterval2 = data[&#39;day3&#39;] - data[&#39;day2&#39;]
dateInterval3 = data[&#39;day(-2)&#39;] - data[&#39;day(-3)&#39;]
dateInterval4 = data[&#39;day(-1)&#39;] - data[&#39;day(-2)&#39;]

data[&#39;dateInterval&#39;] = (dateInterval1+dateInterval2+dateInterval3+dateInterval4)/4
data.loc[only5, &#39;dateInterval&#39;] = (dateInterval1+dateInterval3+dateInterval4)/3
data.loc[only4, &#39;dateInterval&#39;] = (dateInterval3+dateInterval4)/2
data[&#39;dateInterval&#39;] = data[&#39;dateInterval&#39;].dt.days

# 분포
sns.distplot(data[&#39;dateInterval&#39;])</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/7d535f40-fa0c-453d-8d68-cc5311400406/image.png" alt=""></p>
<p>상당히 큰 값을 가진 데이터들이 존재하여 로그변환을 수행하였다. 값이 0인 데이터는 로그를 취할 수 없어 -1로 정의하였다.</p>
<pre><code class="language-python"># 로그변환
data[&#39;dateInterval&#39;] = np.log(data[&#39;dateInterval&#39;][data[&#39;dateInterval&#39;] != 0.0])
data[&#39;dateInterval&#39;][data[&#39;dateInterval&#39;] == 0] = -1
sns.distplot(data[&#39;dateInterval&#39;])</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/7608fb07-6518-4089-896f-6a5dd953a471/image.png" alt=""></p>
<h3 id="typegenre">typeGenre</h3>
<p>스토리, 에피소드, 옴니버스의 type장르는 원핫인코딩을 하였다. type_스토리 변수의 해당 웹툰이 스토리 장르이면 1, 아니면 0의 값을 갖는다.</p>
<pre><code class="language-python">pd.get_dummies(data[&#39;typeGenre&#39;], prefix = &#39;type&#39;)</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/a2e232c1-3109-4baf-a5d7-35a1791522c5/image.png" alt=""></p>
<h3 id="contentgenre">contentGenre</h3>
<p>daily, comic, fantasy, action, drama, pure, sensibility, thrill, historical, sports의 content장르도 마찬가지로 변환하였다.</p>
<p>+) 실제 프로젝트에서는 R에서 glm(일반화 선형모델)을 돌려 중요도 기준으로 장르를 하나씩만 선택하였다. 아래는 R 코드이다. (이부분은 아직 공부가 더 필요하다.)
결과만 말하자면, 장르의 중요도는 thrill &gt; daily &gt; fantasy &gt; comic &gt; drama &gt; pure &gt; action &gt; sensibility &gt; sports &gt; historical 이었다.</p>
<pre><code class="language-R">library(data.table)
library(dplyr)
library(MASS)
library(stringr)

webtoon = read.csv(&#39;./mywebtoon_data.csv&#39;, header=T, fileEncoding=&#39;utf-8&#39;)
webtoon$isPublic &lt;- as.factor(webtoon$isPublic)

# 장르 개수 입력
webtoon$numGenre &lt;- 1
for (i in 1:length(webtoon$contentGenre)) {
  genre = strsplit(webtoon$contentGenre, split=&quot;,&quot;)[i][[1]]
  while (length(genre) &gt; webtoon$numGenre[i]) {
    webtoon$numGenre[i] &lt;- webtoon$numGenre[i] + 1
  }
}

# 중요도 계산
cont_list = c(&#39;action&#39;,&#39;comic&#39;,&#39;daily&#39;,&#39;drama&#39;,&#39;fantasy&#39;,&#39;historical&#39;,&#39;pure&#39;,&#39;sensibility&#39;,&#39;sports&#39;,&#39;thrill&#39;)
fit_genre_thrill &lt;- glm(isPublic ~ factor(webtoon$contentGenre[webtoon$numGenre==1], levels=cont_list[c(10,1:9)]),
                        family=binomial, data=webtoon[webtoon$numGenre==1,])
fit_genre_thrill %&gt;% summary()

# p-value 가 작은 순서대로 지정
webtoon$oneGenre &lt;- webtoon$contentGenre
cont_pval_list = c(&#39;thrill&#39;,&#39;daily&#39;,&#39;fantasy&#39;,&#39;comic&#39;,&#39;drama&#39;, &#39;pure&#39;,&#39;action&#39;,&#39;sensibility&#39;,&#39;sports&#39;,&#39;historical&#39;)
for (i in 1:length(webtoon$oneGenre)){
  for (cont in cont_pval_list) {
    genre = str_detect(webtoon$contentGenre[i], cont)
    if (genre) {
      webtoon$oneGenre[i] &lt;- cont
      break
    }
  }
}</code></pre>
<h1 id="❓초반과-후반-데이터의-파생변수를-고려한-이유">❓초반과 후반 데이터의 파생변수를 고려한 이유</h1>
<p>초반과 후반 회차 데이터을 그대로 변수로 사용하지 않고, 차이 혹은 비율의 파생변수를 고려한 이유는 다음과 같다. 첫번째 이유는, 후반 회차의 데이터의 수집 목적이 뒤늦게 인기를 얻는 웹툰과 타 웹툰의 차이를 줄이고자 하는 것이었기 때문이다. 즉, 초반 회차의 데이터 그 자체보다 후반에 비해 초반에 얼마나 큰 주목을 끌었나를 변수화시키고 싶었다. 두번째 이유는 아래 그래프(초반과 후반의 조회수 산점도)를 통해 <span style="color:red">&#39;조회수가 높은 것보다, 조회수를 후반까지 유지하는 것이 더 중요함&#39;</span>을 발견했기 때문이다.</p>
<pre><code class="language-python"># viewsEarly vs. viewsLater
dt1 = data[data[&#39;isPublic&#39;] == 0][[&#39;viewsEarly&#39;,&#39;viewsLater&#39;]]
dt2 = data[data[&#39;isPublic&#39;] == 1][[&#39;viewsEarly&#39;,&#39;viewsLater&#39;]]
plt.scatter(dt1[&#39;viewsEarly&#39;], dt1[&#39;viewsLater&#39;], c=&#39;green&#39;)
plt.scatter(dt2[&#39;viewsEarly&#39;], dt2[&#39;viewsLater&#39;], c=&#39;red&#39;)
plt.xlabel(&#39;viewsEarly&#39;)
plt.ylabel(&#39;viewsLater&#39;)
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/c9a4b537-d6b0-4345-9615-86e01200420d/image.png" alt=""></p>
<p>그래프에서 빨간색은 정식연재, 초록색은 정식연재가 아닌 웹툰이다. 빨간색 점들은 y=x 그래프꼴로 선형관계를 보이는 반면, 초록색 점들은 아래로 처지는 점들이 많이 보인다. 이를 통해 조회수가 초반이 높고 후반은 낮은 웹툰들은 정식연재로 승격되지 않는 것을 알 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[베스트도전 웹툰의 정식연재 승격 확률 예측 - 2. 회차 크롤링]]></title>
            <link>https://velog.io/@goodjin_55/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B2%A0%EC%8A%A4%ED%8A%B8%EB%8F%84%EC%A0%84-%EC%9B%B9%ED%88%B0%EC%9D%98-%EC%A0%95%EC%8B%9D%EC%97%B0%EC%9E%AC-%EC%8A%B9%EA%B2%A9-%ED%99%95%EB%A5%A0-%EC%98%88%EC%B8%A1-2.-%ED%9A%8C%EC%B0%A8-%ED%81%AC%EB%A1%A4%EB%A7%81</link>
            <guid>https://velog.io/@goodjin_55/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B2%A0%EC%8A%A4%ED%8A%B8%EB%8F%84%EC%A0%84-%EC%9B%B9%ED%88%B0%EC%9D%98-%EC%A0%95%EC%8B%9D%EC%97%B0%EC%9E%AC-%EC%8A%B9%EA%B2%A9-%ED%99%95%EB%A5%A0-%EC%98%88%EC%B8%A1-2.-%ED%9A%8C%EC%B0%A8-%ED%81%AC%EB%A1%A4%EB%A7%81</guid>
            <pubDate>Sun, 05 Feb 2023 05:01:34 GMT</pubDate>
            <description><![CDATA[<p>이전에 수집한 웹툰들의 리스트를 바탕으로 각 초반 3화, 후반 3화의 정보를 크롤링할 것이다.</p>
<h1 id="패키지-import">패키지 import</h1>
<pre><code class="language-python">import pandas as pd
import time

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.common.alert import Alert
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC</code></pre>
<p>수집을 진행하기 전에, webdriver에 url을 넣어주어야 한다.</p>
<pre><code class="language-python">wd = webdriver.Chrome(&#39;C:/chromedriver.exe&#39;)
url = &quot;https://comic.naver.com/bestChallenge/list?titleId=&quot; + str(webtoon.loc[i,&#39;titleId&#39;])
wd.get(url)</code></pre>
<h1 id="❤️-하트수">❤️ 하트수</h1>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/39a2b991-3b98-45dc-abc9-b6645100d12f/image.png" alt=""></p>
<p>하트수가 나와있는 버튼을 우클릭 -&gt; 검사 하여 개발자도구를 연다.</p>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/8a03f95b-5924-4179-b182-14d4a8270c23/image.png" alt=""></p>
<p> &#39;9,210&#39;에서 <span style="color:darkblue">우클릭 -&gt; Copy -&gt; Copy selector</span> 하여 find_element 함수의 인자로 넣을 것이다.</p>
<h3 id="예외-시리즈-작품">예외) 시리즈 작품</h3>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/db062bc9-8150-43cc-8856-fb73856424b2/image.png" alt=""></p>
<p>위 웹툰처럼, 시리즈 작품은 기존 하트수가 있는 버튼이 &#39;시리즈에서 보기&#39; 버튼에 의해 한 칸 오른쪽에 있었다. 따라서 기존의 방법처럼 Copy selector 를 진행하여 따로 입력하였다.
시리즈 작품의 여부는 개발자 도구에서 &#39;시리즈에서 보기&#39; 버튼의 유무로 판단하였다.</p>
<pre><code class="language-python">try: # 시리즈 작품의 하트수 추출
    series = &#39;시리즈&#39; in wd.find_element(By.CSS_SELECTOR, &#39;#content &gt; div.comicinfo &gt; div.detail &gt; ul &gt; li:nth-child(4) &gt; a &gt; span&#39;).text
    heart = wd.find_element(By.CSS_SELECTOR,&#39;#content &gt; div.comicinfo &gt; div.detail &gt; ul &gt; li:nth-child(6) &gt; div &gt; a &gt; em&#39;).text
except: # 시리즈가 아닌 작품의 하트수 추출
    heart = wd.find_element(By.CSS_SELECTOR,&#39;#content &gt; div.comicinfo &gt; div.detail &gt; ul &gt; li:nth-child(5) &gt; div &gt; a &gt; em&#39;).text</code></pre>
<h1 id="📚-type장르">📚 type장르</h1>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/91bdc61d-eb82-4f55-9616-36cd623dab04/image.png" alt=""></p>
<p>에피소드/옴니버스/스토리 의 type 장르는 <span style="color:darkblue">class = &quot;on&quot;</span> 인 클래스로 확인할 수 있다. 마찬가지로 find_element를 통해 정보를 추출하였다.</p>
<pre><code class="language-python">typeGenre = wd.find_element(By.CSS_SELECTOR,&#39;#content &gt; div.snb &gt; ul &gt; li.on&#39;).text</code></pre>
<h1 id="✔️-회차별-별점-별점참여수-등록일-조회수">✔️ 회차별 별점, 별점참여수, 등록일, 조회수</h1>
<p><img src="https://velog.velcdn.com/images/goodjin_55/post/a5b52332-1269-4218-a26a-4617049ad9ea/image.png" alt=""></p>
<p>위 사진에서 find_element를 이용해 별점, 별점참여수, 등록일, 조회수를 수집하였다.</p>
<pre><code class="language-python">star = wd.find_element(By.CSS_SELECTOR, &#39;#topPointTotalNumber&#39;).text
starPar = wd.find_element(By.CSS_SELECTOR, &#39;#topTotalStarPoint &gt; span.pointTotalPerson &gt; em&#39;).text
views(-1) = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(4)&#39;).text
day(-1) = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(2)&#39;).text</code></pre>
<h1 id="💬-회차별-댓글">💬 회차별 댓글</h1>
<p>전체 댓글을 가져오기 힘들 것 같아 &#39;BEST댓글&#39;을 최대 5개로 수집하였다. 각 댓글의 좋아요수, 싫어요수도 중요한 변수가 될 것 같아 같이 수집하였다.</p>
<pre><code class="language-python">comment = pd.DataFrame(columns= [&#39;titleId&#39;,&#39;isPublic&#39;,&#39;comment&#39;,&#39;like&#39;,&#39;hate&#39;])
# 댓글창 열기
wd.switch_to.frame(&#39;commentIframe&#39;) 
# &#39;BEST댓글&#39;이 없는 경우 == &#39;전체댓글&#39;로 설정되어 있는 경우
if wd.find_element(By.CSS_SELECTOR,&#39;#cbox_module &gt; div &gt; div.u_cbox_sort &gt; div.u_cbox_sort_option &gt; div &gt; ul &gt; li.u_cbox_sort_option_wrap.u_cbox_sort_option_on&#39;).text == &#39;전체댓글&#39;:
    wd.switch_to.default_content(); wd.back(); continue
# &#39;BEST댓글&#39; 수집
commentNum = min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5)
for j in range(commentNum): #댓글수가 5개 미만인 경우 고려
    comment.loc[len(comment)] = [titleId, isPublic, 
                                wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)[j].text,
                                wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_recomm&#39;)[j].text,
                                wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_unrecomm&#39;)[j].text]
wd.switch_to.default_content(); wd.back()
</code></pre>
<h1 id="💻-전체코드">💻 전체코드</h1>
<p>변수에서 숫자는 각 회차의 정보를 의미한다. 예를 들어 star1은 1화, star2는 2화, ..., star(-1)은 가장 최신화 이다. 제목에 &#39;공지&#39;가 있는 회차는 수집 목적에 맞지 않다고 판단하여 해당 회차는 건너뛰었다.
수집 순서는 가장 최신화 -&gt; 두번째 최신화 -&gt; 세번째 최신화 -&gt; &#39;첫화보기&#39;를 클릭하여 첫화 -&gt; 두번째 화 -&gt; 세번째 화 이다. 
코드의 중간중간에(url 이동 후, 댓글창 이동 후 등) time.sleep() 함수를 이용하여, 에러를 줄일 수 있다. </p>
<pre><code class="language-python"># import
import pandas as pd
import time

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.common.alert import Alert
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# 각 웹툰별 정보 크롤링
comment = pd.DataFrame(columns= [&#39;titleId&#39;,&#39;isPublic&#39;,&#39;comment&#39;,&#39;like&#39;,&#39;hate&#39;])
alertList = [] # 경고창 때문에 첫번째 회차가 안 열리는 경우 : 따로 크롤링 진행!
wd = webdriver.Chrome(&#39;C:/chromedriver.exe&#39;)

for i in range(len(webtoon)):
    # url이동
    url = &quot;https://comic.naver.com/bestChallenge/list?titleId=&quot; + str(webtoon.loc[i,&#39;titleId&#39;])
    wd.get(url)

    # 하트수
    try: # 시리즈 작품
        series = &#39;시리즈&#39; in wd.find_element(By.CSS_SELECTOR, &#39;#content &gt; div.comicinfo &gt; div.detail &gt; ul &gt; li:nth-child(4) &gt; a &gt; span&#39;).text
        while True: 
            # url 이동 후 바로 버튼이 생성되지 않아 반복문 이용. time.sleep()을 이용해도 좋다.
            try: 
                webtoon.loc[i,&#39;heart&#39;] = wd.find_element(By.CSS_SELECTOR,&#39;#content &gt; div.comicinfo &gt; div.detail &gt; ul &gt; li:nth-child(6) &gt; div &gt; a &gt; em&#39;).text
                break
            except: pass
    except: # 시리즈 작품이 아닌 작품
        while True:
            try: 
                webtoon.loc[i,&#39;heart&#39;] = wd.find_element(By.CSS_SELECTOR,&#39;#content &gt; div.comicinfo &gt; div.detail &gt; ul &gt; li:nth-child(5) &gt; div &gt; a &gt; em&#39;).text
                break
            except: pass

    # 에피소드/옴니버스/스토리
    webtoon.loc[i,&#39;typeGenre&#39;] = wd.find_element(By.CSS_SELECTOR,&#39;#content &gt; div.snb &gt; ul &gt; li.on&#39;).text

    pageCnt = len(wd.find_elements(By.CSS_SELECTOR, &#39;#content &gt; table &gt; tbody &gt; tr &gt; td.title &gt; a&#39;)) # 첫 화면에서의 회차 수 &lt;= 10
    ## 3화 이하인 웹툰은 정보가 너무 적다고 판단하여 수집하지 않음.
    if pageCnt &lt;= 3: continue 

    tryCnt = 0 # 수집을 도전한 회차 수
    dataCnt = 0 # 수집한 회차 수
    isalert = False # 경고창 팝업 여부

    for no in range(0,pageCnt):
        if (dataCnt == 3) or (tryCnt == pageCnt): break
        tryCnt += 1

        ## 최근 날짜 맞추기위해 실행 
        if (no == 0) and (pd.to_datetime(wd.find_elements(By.CSS_SELECTOR, &#39;#content &gt; table &gt; tbody &gt; tr &gt; td.num&#39;)[0].text) &gt; pd.to_datetime(&#39;2022.12.02&#39;)): continue 

        ## &#39;공지&#39; 글자가 있는 회차 스킵
        title = wd.find_elements(By.CSS_SELECTOR,&#39;#content &gt; table &gt; tbody &gt; tr &gt; td.title &gt; a&#39;)[no].text
        if &#39;공지&#39; in title: continue

        ## 가장 최신 화 클릭
        wd.find_elements(By.CSS_SELECTOR,&#39;#content &gt; table &gt; tbody &gt; tr &gt; td.title &gt; a&#39;)[no].click()
        dataCnt += 1

        if dataCnt == 1: ## 최근 1화
            webtoon.loc[i,&#39;star(-1)&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#topPointTotalNumber&#39;).text
            webtoon.loc[i,&#39;starPar(-1)&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#topTotalStarPoint &gt; span.pointTotalPerson &gt; em&#39;).text
            webtoon.loc[i,&#39;views(-1)&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(4)&#39;).text
            webtoon.loc[i,&#39;day(-1)&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(2)&#39;).text

            ## 댓글
            wd.switch_to.frame(&#39;commentIframe&#39;)
            if wd.find_element(By.CSS_SELECTOR,&#39;#cbox_module &gt; div &gt; div.u_cbox_sort &gt; div.u_cbox_sort_option &gt; div &gt; ul &gt; li.u_cbox_sort_option_wrap.u_cbox_sort_option_on&#39;).text == &#39;전체댓글&#39;:
                wd.switch_to.default_content(); wd.back(); continue
            commentNum = min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5) #댓글수가 5개 미만인 경우 고려
            for j in range(min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5)): 
                comment.loc[len(comment)] = [webtoon.loc[i,&#39;titleId&#39;],webtoon.loc[i,&#39;isPublic&#39;],
                                             wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)[j].text,
                                             wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_recomm&#39;)[j].text,
                                             wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_unrecomm&#39;)[j].text]
            wd.switch_to.default_content(); wd.back()

        elif dataCnt == 2: ## 최근 2화
            webtoon.loc[i,&#39;star(-2)&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#topPointTotalNumber&#39;).text
            webtoon.loc[i,&#39;starPar(-2)&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#topTotalStarPoint &gt; span.pointTotalPerson &gt; em&#39;).text
            webtoon.loc[i,&#39;views(-2)&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(4)&#39;).text
            webtoon.loc[i,&#39;day(-2)&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(2)&#39;).text

            wd.switch_to.frame(&#39;commentIframe&#39;) # 댓글 크롤링
            if wd.find_element(By.CSS_SELECTOR,&#39;#cbox_module &gt; div &gt; div.u_cbox_sort &gt; div.u_cbox_sort_option &gt; div &gt; ul &gt; li.u_cbox_sort_option_wrap.u_cbox_sort_option_on&#39;).text == &#39;전체댓글&#39;:
                wd.switch_to.default_content(); wd.back(); continue
            commentNum = min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5) #댓글수가 5개 미만인 경우 고려
            for j in range(min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5)): 
                comment.loc[len(comment)] = [webtoon.loc[i,&#39;titleId&#39;],webtoon.loc[i,&#39;isPublic&#39;],
                                             wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)[j].text,
                                             wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_recomm&#39;)[j].text,
                                             wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_unrecomm&#39;)[j].text]
            wd.switch_to.default_content(); wd.back()

        elif dataCnt == 3: ## 최근 3화
            webtoon.loc[i,&#39;star(-3)&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#topPointTotalNumber&#39;).text
            webtoon.loc[i,&#39;starPar(-3)&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#topTotalStarPoint &gt; span.pointTotalPerson &gt; em&#39;).text
            webtoon.loc[i,&#39;views(-3)&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(4)&#39;).text
            webtoon.loc[i,&#39;day(-3)&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(2)&#39;).text

            wd.switch_to.frame(&#39;commentIframe&#39;) # 댓글 크롤링
            if wd.find_element(By.CSS_SELECTOR,&#39;#cbox_module &gt; div &gt; div.u_cbox_sort &gt; div.u_cbox_sort_option &gt; div &gt; ul &gt; li.u_cbox_sort_option_wrap.u_cbox_sort_option_on&#39;).text == &#39;전체댓글&#39;:
                wd.switch_to.default_content(); continue
            commentNum = min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5) #댓글수가 5개 미만인 경우 고려
            for j in range(min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5)): 
                comment.loc[len(comment)] = [webtoon.loc[i,&#39;titleId&#39;],webtoon.loc[i,&#39;isPublic&#39;],
                                             wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)[j].text,
                                             wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_recomm&#39;)[j].text,
                                             wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_unrecomm&#39;)[j].text]
            wd.switch_to.default_content()

    if (tryCnt == pageCnt): continue

    ## 1화
    try:
        tryCnt += 1
        wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.comicinfo &gt; div.detail &gt; ul &gt; li:nth-child(2) &gt; a&#39;).click()
        alert = Alert(wd); alert.accept()
        alertList += [i]
        continue
    except: pass

    title = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.view &gt; h3&#39;).text
    while (&#39;공지&#39; in title) and (tryCnt &lt; pageCnt):
        tryCnt += 1
        wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.view &gt; div &gt; span.next &gt; a&#39;).click()
        title = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.view &gt; h3&#39;).text

    webtoon.loc[i,&#39;star1&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#topPointTotalNumber&#39;).text
    webtoon.loc[i,&#39;starPar1&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#topTotalStarPoint &gt; span.pointTotalPerson &gt; em&#39;).text
    webtoon.loc[i,&#39;views1&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(4)&#39;).text
    webtoon.loc[i,&#39;day1&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(2)&#39;).text

    wd.switch_to.frame(&#39;commentIframe&#39;) # 댓글 크롤링
    time.sleep(1.5)
    if wd.find_element(By.CSS_SELECTOR,&#39;#cbox_module &gt; div &gt; div.u_cbox_sort &gt; div.u_cbox_sort_option &gt; div &gt; ul &gt; li.u_cbox_sort_option_wrap.u_cbox_sort_option_on&#39;).text == &#39;전체댓글&#39;:
        wd.switch_to.default_content(); continue
    commentNum = min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5) #댓글수가 5개 미만인 경우 고려
    for j in range(min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5)): 
        comment.loc[len(comment)] = [webtoon.loc[i,&#39;titleId&#39;],webtoon.loc[i,&#39;isPublic&#39;],
                                     wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)[j].text,
                                     wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_recomm&#39;)[j].text,
                                     wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_unrecomm&#39;)[j].text]
    wd.switch_to.default_content()
    if tryCnt == pageCnt: continue

    ## 2화 
    tryCnt += 1
    wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.view &gt; div &gt; span.next &gt; a&#39;).click()
    title = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.view &gt; h3&#39;).text
    while (&#39;공지&#39; in title) and (tryCnt &lt; pageCnt):
        tryCnt += 1
        wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.view &gt; div &gt; span.next &gt; a&#39;).click()
        title = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.view &gt; h3&#39;).text

    webtoon.loc[i,&#39;star2&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#topPointTotalNumber&#39;).text
    webtoon.loc[i,&#39;starPar2&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#topTotalStarPoint &gt; span.pointTotalPerson &gt; em&#39;).text
    webtoon.loc[i,&#39;views2&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(4)&#39;).text
    webtoon.loc[i,&#39;day2&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(2)&#39;).text

    wd.switch_to.frame(&#39;commentIframe&#39;) # 댓글 크롤링
    time.sleep(0.5)
    if wd.find_element(By.CSS_SELECTOR,&#39;#cbox_module &gt; div &gt; div.u_cbox_sort &gt; div.u_cbox_sort_option &gt; div &gt; ul &gt; li.u_cbox_sort_option_wrap.u_cbox_sort_option_on&#39;).text == &#39;전체댓글&#39;:
        wd.switch_to.default_content(); continue
    commentNum = min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5) #댓글수가 5개 미만인 경우 고려
    for j in range(min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5)): 
        comment.loc[len(comment)] = [webtoon.loc[i,&#39;titleId&#39;],webtoon.loc[i,&#39;isPublic&#39;],
                                     wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)[j].text,
                                     wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_recomm&#39;)[j].text,
                                     wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_unrecomm&#39;)[j].text]
    wd.switch_to.default_content()
    if tryCnt == pageCnt: continue

    ## 3화
    tryCnt += 1
    wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.view &gt; div &gt; span.next &gt; a&#39;).click()
    title = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.view &gt; h3&#39;).text
    while (&#39;공지&#39; in title) and (tryCnt &lt; pageCnt):
        tryCnt += 1
        wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.view &gt; div &gt; span.next &gt; a&#39;).click()
        title = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.view &gt; h3&#39;).text

    webtoon.loc[i,&#39;star3&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#topPointTotalNumber&#39;).text
    webtoon.loc[i,&#39;starPar3&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#topTotalStarPoint &gt; span.pointTotalPerson &gt; em&#39;).text
    webtoon.loc[i,&#39;views3&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(4)&#39;).text
    webtoon.loc[i,&#39;day3&#39;] = wd.find_element(By.CSS_SELECTOR, &#39;#sectionContWide &gt; div.tit_area &gt; div.vote_lst &gt; dl.rt &gt; dd:nth-child(2)&#39;).text

    wd.switch_to.frame(&#39;commentIframe&#39;) # 댓글 크롤링
    if wd.find_element(By.CSS_SELECTOR,&#39;#cbox_module &gt; div &gt; div.u_cbox_sort &gt; div.u_cbox_sort_option &gt; div &gt; ul &gt; li.u_cbox_sort_option_wrap.u_cbox_sort_option_on&#39;).text == &#39;전체댓글&#39;:
        wd.switch_to.default_content(); continue
    commentNum = min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5) #댓글수가 5개 미만인 경우 고려
    for j in range(min(len(wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)),5)): 
        comment.loc[len(comment)] = [webtoon.loc[i,&#39;titleId&#39;],webtoon.loc[i,&#39;isPublic&#39;],
                                     wd.find_elements(By.CLASS_NAME, &#39;u_cbox_contents&#39;)[j].text,
                                     wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_recomm&#39;)[j].text,
                                     wd.find_elements(By.CLASS_NAME, &#39;u_cbox_cnt_unrecomm&#39;)[j].text]
    wd.switch_to.default_content()

# 웹툰 데이터 csv 파일로 저장
webtoon.to_csv(&#39;mywebtoon_data.csv&#39;, index= False)
comment.to_csv(&#39;mycomment_data.csv&#39;, index= False)</code></pre>
<p>코딩 실력이 부족하여 코드가 상당히 길지만, 잘 정리하여 간략한 코드를 만들어보고 싶다..</p>
]]></description>
        </item>
    </channel>
</rss>