336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

다섯 번째 프로젝트

 

1. 추천 시스템에 대한 생각

2. 추천시스템(TFIDF, Word2Vec)

3. GUI로 앱처럼 사용할 수 있게 (PyQt5)

 


드디어 마무리 단계에 왔다 ㅠㅠㅠ 티스토리 블로그 글을 작성하면서 파이널 프로젝트를 하는데 아직도 모르는게 태산이다. 파이널 프로젝트는 자연어(NLP)로 진행중인데, 왜 국문학과를 찾는지 알것 같다.

 

이번 프로젝트를 할 때, 후기 개수 자체가 부족하고, 과마다 특성 있는 키워드가 영화 리뷰처럼 다양하지 않아서(ex. OOO 원장님 너무 친절하고 좋아요. 아프지 않게 잘해주시고 병원이 깔끔해요. 등등) 힘들 것이라 예상을 했다. 결과는 예상대로 워드클라우드로 돌렸을 때 불용어처리할 것이 엄청 많았다. 그래서 추가적으로 팀원들이 후기를 one sentence로 만들 때 진료과목도 집어넣기는 했는데, 이게 얼마나 효과가 있을지는 잘 모르겠다.

 

이번에는 Django 대신에 PyQt5를 이용해서 GUI 앱을 만들었다. 저번에 해보자고 했는데 드디어.. 완성!

 

 

 

 

 

위키독스

온라인 책을 제작 공유하는 플랫폼 서비스

wikidocs.net

 

위키독스

온라인 책을 제작 공유하는 플랫폼 서비스

wikidocs.net

PyQt5 내용은 위키독스를 통해서 잘 배울 수 있다. 모르는 기능들을 하나하나 찾아서 구현이 됐다. 물론 심각한 노가다가 필요한 작업이라고 생각한다. Django, 안드로이드스튜디오 많은 시간이 필요하다.

 

 

 

 

우선 제일 첫번째 안 (다른 팀원)

- 검색기능 / 진료과목 카테고리 / 지역을 클릭하면 => 추천 병원과 병원 정보가 나오게 하는 것이다. 

 

 

 

 

두번째 안 (나)

- 검색기능 / 진료과목을 라디오버튼으로 클릭 => 추천 병원과 나에게 맞는 병원 리스트 담기 병원 정보 나오게 하기 

 

 

 

세번째 안 (절충안)

- 병원을 모를 수도 있기 때문에, 우선 진료과목과 지역을 먼저 선택하게 했다. 그후에 나오는 병원과 유사한 병원을 추가로 검색하고 싶으면 검색하게 만들었다. 그리고 홈페이지의 경우 url을 직접 복사붙여넣기 하는 것보다 바로가기로 만드는 것이 좋다고 생각하여 따로 뺐다.

 

 

 

 

우선 터미널에서 designer를 입력하여 Qt Designer를 실행시켜 ui를 만든다. 안드로이드스튜디오처럼 자신이 원하는 버튼이나 위젯을 끌어다 쓰고, 클래스 옆에 파이참에서 호출하기 쉬운 objectName을 지정해준다. 

 

우리가 구현했던 기능은 자동완성기능, 필터링 기능(지역 진료과목), url클릭시 바로 웹으로 넘어가기, 리스트내 요소를 클릭하여 정보 받아오는 기능, 추천기능, 리셋기능,  등이다. 간단해 보여도 간단하지 않고 시간이 꽤 걸렸다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QStringListModel
from PyQt5 import uic
import pandas as pd
from sklearn.metrics.pairwise import linear_kernel
from gensim.models import Word2Vec
from scipy.io import mmwrite, mmread
import pickle
import webbrowser
 
form_window = uic.loadUiType('plz_Yes_button_edit_3.ui')[0]
 
class Exam(QWidget, form_window):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
 
###########################################
 
if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = Exam()
    w.show()
    sys.exit(app.exec_())
 
cs

가장 기본적인 구성 - 본인이 만든 ui를 불러오는 코드부터 GUI를 실행시키는 코드까지

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
        #데이터 불러오기
        self.df_review = pd.read_csv('./datasets/model_data_Hospital_and_info2.csv',index_col=0)
        self.df_review.info()
        self.Tfidf_matrix = mmread('./models/tfidf_hospital_review_l.mtx').tocsr()
        self.embedding_model = Word2Vec.load('./models/word2VecModel_hospital_l2.model')
        with open('./models/tfidf_l.pickle''rb'as f:
            self.Tfidf = pickle.load(f)
 
        # 카테고리 목록 리스트화
        self.cmb_title_2.addItem('과를 선택하세요')
        category = list(self.df_review.category.unique()) #카테고리 중복 없이
        category = sorted(category)
 
        for c in category :
            self.cmb_title_2.addItem(c)
 
        # 지역 목록 리스트화
        self.cmb_title.addItem('지역을 선택하세요')
        add_list = []
        for i in self.df_review.addresses:
            a = i.split(' ')[0#지역이름만
            add_list.append(a)
 
        add_set = set(add_list) #중복 제거 위해 set
        address = list(add_set) #다시 list
        address = sorted(address)
        address.pop(0#지역 아닌 다른 단어가 있어서 pop
 
 
        for add in address:
            self.cmb_title.addItem(add) #지역 목록
 
        # 병원 목록과 진료과목 리스트로 만들기
        total = ''
        for c in self.df_review.clinics:
            total += c
 
        totals = total.split(', ')
        total_set = set(totals)
        total = list(total_set)  #진료 과목
        total = sorted(total)
 
        titles = list(self.df_review.names) # 병원 이름
        titles = sorted(titles) # 따로 정렬하는 이유는 병원 이름이 먼저 나오게 하기 위해서
 
        key_title = titles + total      #병원 + 진료 과목
 
cs

데이터, Tfidf, Word2Vec 모델을 불러오고 카테고리 리스트, 지역 목록 리스트, 병원 이름과 진료과목을 리스트로 만들었다. 공간을 크게 차지하지 않고 옵션을 선택할 수 있는 QComboBox에 리스트를 추가했다. 카테고리와 병원 목록은 쉽게 만들 수 있었으나, 지역의 경우 통일이 되지 않아서 따로 전처리를 하였다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#주소 통일
add_dic = {'강원도' : '강원','경기도':'경기','경상남도':'경남','경상북도':'경북','광주광역시':'광주','대구광역시':'대구','대전광역시''대전',
           '부산광역시''부산''서울시' :'서울','서울특별시':'서울','서초구':'서울시 서초구','수원시':'경기','인천광역시':'인천','전라북도':'전북','제주특별자치도':'제주','충청북도':'충북'}
add_list = list(add_dic)
 
list = []
for i in df_review.addresses :
    a = i.split(' ')
    if a[0in add_list :
        a[0= add_dic[a[0]]
    #print(a)
    a = ' '.join(a)
    list.append(a)
df_review.addresses = list
print(df_review.addresses.head(100))
df_review.to_csv('./model_data_Hospital_and_info2.csv')
cs

지역의 경우 17개 시도별로 나누려고 split으로 주소를 나눠 가장 첫번째 글자를 가져왔다. 그렇기 때문에 서울시, 서울, 서울특별시처럼 같은 지역인데 이름이 다를 경우 통일하였다. 다행히 크게 수작업을 해도 되지 않아서 안도의 한숨이...

 

 

 

1
2
3
4
5
        #자동완성
        model = QStringListModel()
        model.setStringList(list(key_title))
        completer = QCompleter()
        completer.setModel(model)
cs

자동완성 기능 생각보다 어렵지 않았다. from PyQt5.QtCore import QStringListModel 불러오고 QCompleter로 자동완성을 시켜주고 이걸 QLineEdit으로 받아줬다.

 

 

 

1
2
3
4
5
6
7
8
9
        # 버튼 함수
        self.le_title.setCompleter(completer)
        self.le_title.returnPressed.connect(self.btn_recommend_slot)
        self.btn_recommend.clicked.connect(self.btn_recommend_slot) # 엔터 또는 버튼 클릭시
        self.cmb_title_2.currentIndexChanged.connect(self.cmb_title_slot_2)
        self.cmb_title.currentIndexChanged.connect(self.cmb_title_slot)
        self.listWidget.itemClicked.connect(self.hospital_info)
        self.btn_html.clicked.connect(self.open_web)
        self.btn_recommend_5.clicked.connect(self.btn_clicked)
cs

버튼을 클릭하거나, 엔터를 누르거나, index가 변할 때 어떤 식으로 작동할지를 구현하는 준비단계?

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    # 병원 지역별로 필터링
    def cmb_title_slot(self):
        print('지역 선택 클릭')
        self.le_title.clear() # 먼저 병원을 클릭 했을 때
        title = self.cmb_title_2.currentText()
        address = self.cmb_title.currentText()
 
        region = self.df_review[(self.df_review.category == title) &(self.df_review.region == address)].iloc[:101# 자체 추천 순위로 출력
        recommend = list(region)
        #print(recommend)
 
        self.listWidget.clear()
        self.listWidget.insertItems(0, recommend)
 
 
    # 카테고리 탑10 병원
    def cmb_title_slot_2(self):
        print('과 선택 클릭')
        title = self.cmb_title_2.currentText()
 
        top = self.df_review[self.df_review.category == title].iloc[:10,1]
        #recommend = '\n'.join(list(top)) # 이거는 lbl_result에
        recommend = list(top)
 
        self.listWidget.clear()
        self.listWidget.insertItems(0, recommend)
cs

지역 필터링, 병원 필터링 currentIndexChanged, index가 바뀌면 함수가 실행이 된다. 원래 cmb_title_slot1과 2를 바꿔야 하는데 처음에 설정을 그렇게 해서 지나갔다. 

 

현재 QComboBox에서 선택한 텍스트를 가져와야 필터링이 되기 때문에, currentText로 현재 텍스트를 가져왔다.  그리고 선택한  진료과목과 일치하는 탑10 병원을 listWidget에 출력하였다. QLineEdit으로 해도 되지만 나중에 병원 정보를 클릭해야 하기 때문에 안 된다. 그리고 list로 받아와서 0 번째 부터 다시 보여주는 코드를 실행했다. 마찬가지로 지역 설정을 바꿨을 경우, 우선 진료과목과 지역을 동시에 만족하는 병원을 추천하였다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    # 병원을 클릭 했을 때, 병원 정보 보여주기
    def hospital_info(self):
        print('병원 정보 클릭')
        title = self.listWidget.currentItem().text()
 
        try :
            a = self.df_review[self.df_review.names == title].iloc[03].split(',')[:10# 주요 진료 과목 10개만
            a = ','.join(a)
            b = self.df_review[self.df_review.names == title].iloc[04# 주소
            c = self.df_review[self.df_review.names == title].iloc[06# 전화번호
            #d = self.df_review[self.df_review.names == title].iloc[0, 5] # 홈페이지 url / 홈페이지 오픈 버튼으로 대체
            recommend = '[ 주요 진료 과목 ]\n{0}\n\n[ 주소 ]\n{1}\n\n[ 전화번호 ]\n{2}'.format(a, b, c)
            self.infotext.setText(recommend)
            recommend = '홈페이지 바로가기 클릭!'
            self.btn_html.setText(recommend)
        except :
            pass
 
cs

어떻게 listWidget에 있는 아이템을 가져오나 했더니, 텍스트를 가져오는 것과 비슷하게 currentItem()으로 텍스트를 불러올 수 있었다.  크롤링을 할 때 조금 멍청하게 가져와서 전처리 과정을 한 번 더 거쳤다. 진료과목을 리스트 안에 집어넣었는데 이것을 그대로 csv에 집어넣어서 '[' 리스트가 문자가 됐다. 

 

a는 진료과목, b는 주소,  c는 전화번호, d는 홈페이지 url을 보여주려고 했으나 사용자가 따로 긁어야하는 불편함이 있어서 바로 홈페이지 오픈을 시켜주기로 했다. 

 

 

 

 

1
2
3
4
5
6
    # 홈페이지 오픈
    def open_web(self):
        print('홈페이지 바로가기 클릭')
        title = self.listWidget.currentItem().text()
        html = self.df_review[self.df_review.names == title].iloc[05]
        webbrowser.open(html) # 홈페이지 연동
cs

검색을 해보니 엄청 복잡하게 url을 오픈 하는 경우가 있는데, import webbroser를 하고 html을 open 하면 바로 url로 넘어가진다코딩을 하다보면 어떻게 하면 더 코드를 간단하게 짤까 고민을 하면서, 다른 사람이 짜놓은 코드와 비교를 하다보면 가끔 감탄을 할 때가 온다. 나도 누가 코드를 보고 감탄했으면 좋겠다는 마음이.. 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    def btn_recommend_slot(self):
        print('추천 시스템 클릭')
        title = self.le_title.text()
 
        try:
            if title in list(self.df_review['names']):
                h_idx = self.df_review[
                    self.df_review['names']==title].index[0]
                cosine_sim = linear_kernel(
                    self.Tfidf_matrix[h_idx],
                    self.Tfidf_matrix)
                # recommend = '\n'.join(
                #     list(self.getRecommendation(cosine_sim))[1:])
                recommend = list(self.getRecommendation2(cosine_sim))[:-1]
 
            #elif title in total :
 
 
            else:
                print(title, '예외 키워드')
                sentence = [title] * 10
 
                sim_word = self.embedding_model.wv.most_similar(title, topn=10)
                labels = []
                for label, _ in sim_word:
                    labels.append(label)
                print(labels)
 
                for i, word in enumerate(labels):
                    sentence += [word] * (9 - i)
 
                sentence = ' '.join(sentence)
                sentence_vec = self.Tfidf.transform([sentence])
                cosine_sim = linear_kernel(sentence_vec,
                                           self.Tfidf_matrix)
                # recommend = '\n'.join(
                #     list(self.getRecommendation(cosine_sim))[:-1])
 
                recommend = list(self.getRecommendation2(cosine_sim))[:-1]
        except:
            if title :
                recommend =['검색어를 다시 확인해주세요']
                self.infotext.clear()
 
                default_text = '[ 주요 진료 과목 ]\n\n[ 주소 ]\n\n[ 전화번호 ]'
                self.infotext.setText(default_text)
 
            else:
                pass
        self.listWidget.clear()
        self.listWidget.insertItems(0, recommend)
cs

병원명을 정확히 입력했을 때와 진료과목 또는 다른 키워드를 입력했을 때, 그리고 검색어를 입력하지 않았을 때를 나눠서 진행했다. 

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    #키워드 기반 추천 시스템
    def getRecommendation2(self, cosine_sim):
        title = self.cmb_title_2.currentText()
        address = self.cmb_title.currentText()
        print(title, address)
        simScores = list(enumerate(cosine_sim[-1]))
        simScores = sorted(simScores, key=lambda x: x[1], reverse=True)
 
        if title == '과를 선택하세요' and address =='지역을 선택하세요':
            pass
        else :
            simlist = []
            for i in simScores :
                add = self.df_review.iloc[i[0],7# 지역
                tit = self.df_review.iloc[i[0],0# 카테고리
 
                if add == address and tit == title : # 지역, 카테고리 동시에 일치할 때만 추가
                    #print(add)
                    simlist.append(i)
 
            h_idx = [i[0for i in simlist[0:10]]
 
            if len(h_idx) == 0:
                RecHosptiallist = [f'{address} 지역에는 관련된 키워드가 없습니다.']
                return RecHosptiallist.names
            else :
                RecHosptiallist = self.df_review.iloc[h_idx]
                print(RecHosptiallist, '출력')
                return RecHosptiallist.names
 
        simScores = simScores[0:11]
        h_idx = [i[0for i in simScores]
        RecHosptiallist = self.df_review.iloc[h_idx]
        return RecHosptiallist.names
cs

검색을 했을 경우 유사한 병원을 추천하긴 하는데, 전혀 다른 진료과목이나 지역이 나오면 안 되기 때문에 여기서도 필터링 기능을 사용하였다. 모델의 아쉬운 점은 지방으로 갈 수록 후기도 많지 않고, 대부분 큰 병원들은 수도권에 몰려서 지방으로 갈수록 추천해줄만한 병원이 많지 않았다. 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    #리셋 기능
    def btn_clicked(self):
        print('리셋 버튼 클릭')
        self.cmb_title_2.clear()
        self.cmb_title.clear()
        self.le_title.clear()
        self.infotext.clear()
 
        #병원 정보 처음에 나오는 내용
        default_text = '[ 주요 진료 과목 ]\n\n[ 주소 ]\n\n[ 전화번호 ]'
        self.infotext.setText(default_text)
 
        category = list(self.df_review.category.unique())
        category = sorted(category)
        self.cmb_title_2.addItem('과를 선택하세요')
        self.cmb_title.addItem('지역을 선택하세요')
 
        add_list = []
        for i in self.df_review.addresses:
            a = i.split(' ')[0]
            add_list.append(a)
 
        add_set = set(add_list)
        address = list(add_set)
        address = sorted(address)
        address.pop(0)
 
        for add in address:
            self.cmb_title.addItem(add)  # 지역 목록
 
        for c in category:
            self.cmb_title_2.addItem(c)  # 카테고리 목록
cs

리셋버튼을 누르면 모든 것이 리셋이 되게 하려고 했다. 찾아보니 그런 코드가 있긴 있는데, 지금 GUI의 경우 지역 목록과 카테고리 목록은 남아야 해서 리셋을 누르는 동시에 다시 지역목록과 카테고리 목록이 뜨게 했다. 여기서 특히 오류가 많이 났는데, 어디서 호출을 해주느냐에 따라서 이게 중복 노출이 되는지, 부분 노출이 되는지 갈렸다.

 

아무튼 GUI 완성 ㅠㅠ

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

다섯 번째 프로젝트

 

1. 추천 시스템에 대한 생각

2. 추천시스템(TFIDF, Word2Vec)

3. GUI로 앱처럼 사용할 수 있게 (PyQt5)


항상 과정이 비슷해서 앞에 부분은 코드는 생략합니다.

 

우선 로그인이 필요한 서비스라서 동적 크롤링으로 후기를 긁어왔다. 그리고 추가로 병원 이름, 클리닉명, 주소, 링크, 전화번호는 로그인 없이도 크롤링이 가능해서 BS4로 긁어왔다. 과정은 크롤링한 데이터를 전처리하고, 워드클라우드를 생성해서 불용어를 처리하고(중요), 하나의 문장으로 합쳤다.  

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from scipy.io import mmwrite, mmread #매트릭스 저장할 때 mmwrite, 읽을 땐 mmread
import pickle #변수 타입 그대로 저장
 
df_review_one_sentence = pd.read_csv('./preprocess/total_hospital_review_one_sentence.csv', index_col=0)
print(df_review_one_sentence.info())
 
Tfidf = TfidfVectorizer(sublinear_tf=True)      # sublinear_tf는 값의 스무딩 여부를 결정하는 파라미터
Tfidf_matrix = Tfidf.fit_transform(df_review_one_sentence['reviews'])       # fit_transform 된 Tfidf를 갖고 있으면 추후 데이터 추가 가능하므로 따로 저장
 
with open('./models/tfidf.pickle''wb'as f:
    pickle.dump(Tfidf, f)       # Tfidf 저장
 
mmwrite('./models/tfidf_hospital_review.mtx', Tfidf_matrix)        # 유사도 점수 매트릭스 저장
cs

TfidfVectorizer로 문서 내에서 단어 토큰을 생성하고, 각 단어의 수와 가중치를 조정하여 문자를 순서 벡터로 변환했다. 이 과정은 따로 건드리는 부분이 없다. 다만 데이터 전처리가 중요하다.

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import pandas as pd
from sklearn.metrics.pairwise import linear_kernel
from scipy.io import mmwrite, mmread
import pickle
from gensim.models import Word2Vec
 
df_hospital_review_one_sentence = pd.read_csv('./preprocess/total_hospital_review_one_sentence.csv', index_col=0)       # 리뷰 읽어 df 생성
 
Tfidf_matrix = mmread('./models/tfidf_hospital_review.mtx').tocsr()        # matrix 불러오기
with open('./models/tfidf.pickle''rb'as f:      # tfidf 불러오기
    Tfidf = pickle.load(f)
 
def getRecommendation(cosine_sim):      # 코사인 유사도롤 활용하여 유사한 병원 추천하는 함수
    simScore = list(enumerate(cosine_sim[-1]))      # 각 코사인 유사도 값에 인덱스 붙임
    simScore = sorted(simScore, key=lambda x:x[1], reverse=True)        # simScore(코사인 유사도, x[1])가 큰 것부터 정렬. reverse=True 내림차순 정렬.
    simScore = simScore[1:11]       # 유사한 병원 10개 리스트. 0번 값은 자기 자신이므로 배제.
    hospital_idx = [i[0for i in simScore]     # 인덱스(i[0]) 뽑아서 리스트 생성
   recHospitalList = df_hospital_review_one_sentence.iloc[hospital_idx]        # df에서 해당 병원 리스트 추출
    return recHospitalList
 
 
#병원명 검색
hospital_idx = df_hospital_review_one_sentence[df_hospital_review_one_sentence['names']=='김이비인후과의원'].index[0]      # 병원 이름으로 인덱스 값 찾기
# hospital_idx = 127
# print(df_review_one_sentence.iloc[hospital, 0])
 
cosine_sim = linear_kernel(Tfidf_matrix[hospital_idx], Tfidf_matrix)      # linear_kernel은 각 Tfidf 값을 다차원 공간에 벡터(방향과 거리를 가짐)로 배치한 뒤, 코사인 유사도를 구해줌. cosine = 삼각형의 밑변 / 윗변
                                                                       # 비슷한 영화는 유사한 위치에 배치됨. 유사할수록 각이 줄어드므로 코사인 값이 1에 가까워짐. -1에 가까울수록 반대, 0에 가까울수록 무관
recommendation = getRecommendation(cosine_sim)
# print(recommendation)
print(recommendation.iloc[:, 1])
cs

pickle에 저장한 Tfidf를 불러와서 코사인 유사도를 활용하여 유사한 병원을 추천해준다. 우선 병원의 idx 값을 구한다. 그리고 추천시스템에 이와 유사한 병원들의 리스트를 반환 받아서 출력한다.

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pandas as pd
from gensim.models import Word2Vec
 
review_word = pd.read_csv('./preprocess/total_cleaned_review.csv', index_col=0)
print(review_word.info())
cleaned_token_review = list(review_word['cleaned_sentences'])
print(len(cleaned_token_review))
cleaned_tokens = []
count = 0
for sentence in cleaned_token_review:
    token = sentence.split(' ')     # 띄어쓰기 기준으로 각 단어 토큰화
    cleaned_tokens.append(token)
# print(len(cleaned_tokens))
# print(cleaned_token_review[0])
# print(cleaned_tokens[0])
embedding_model = Word2Vec(cleaned_tokens, vector_size=100, window=4,
                           min_count=20, workers=4, epochs=100, sg=1)   # vector_size는 몇차원으로 줄일지 지정, window는 CNN의 kernel_size 개념, 앞뒤로 고려하는 단어의 개수를 나타냄
                                                                        # min_count는 출현 빈도가 20번 이상인 경우만 word track에 추가하라는 의미(즉, 자주 안 나오는 단어는 제외)
                                                                        # workers는 cpu 스레드 몇개 써서 작업할 건지 지정, sg는 어떤 알고리즘 쓸건지 지정
embedding_model.save('./models/word2VecModel_hospital.model')
print(embedding_model.wv.vocab.keys())
print(len(embedding_model.wv.vocab.keys()))
cs

Word2Vec 단어의 의미를 다차원에 분산시켜 표현하여, 단어간 위치를 파악해서 유사도를 분석해준다. Skip-Gram의 경우 중심 단어를 보고 주변에 어떤 단어가 있을지 예측. CBoW는 주변에 있는 단어들로 중간에 있는 단어를 예측한다.

 

 

 

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#리뷰 기반 키워드 검색
embedding_model = Word2Vec.load('./models/word2VecModel_hospital.model')
key_word = '친절'
 
sim_word = embedding_model.wv.most_similar(key_word, topn=10)
labels = []
sentence = []
for label, _ in sim_word:
    labels.append(label)
print(labels)
for i, word in enumerate(labels):
    sentence += [word] * (9-i)
sentence = ' '.join(sentence)
#sentence = [key_word] * 10      # tf에서 높은 값을 가지도록 리스트 복사
print(sentence)
 
sentence_vec = Tfidf.transform([sentence])
cosine_sim = linear_kernel(sentence_vec, Tfidf_matrix)
recommendation = getRecommendation(cosine_sim)
print(recommendation)
cs

키워드를 기반으로 하여 병원을 추천해주는 시스템이다.

 

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

네 번째 프로젝트

 

1. 간단한 intro

2. 웹 크롤링 및 전처리

3. 모델 학습 및 평가


프로젝트를 하면서 느낀 보완사항은 :

-데이터의 길이가 너무 짧으면 단어를 추출하는데 한계가 있고, 과적합이 발생한다.

-유의미한 단어가 존재해야 정확도가 더 개선된다. 모델을 계속 만져봐도 정확도는 크게 개선되지 않는다.

-각 사이트마다 카테고리 분류가 다르게 되어있어서 소비자가 불편할 수도 있을 것이다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 모듈 임포트
import pandas as pd
import numpy as np
from tensorflow.keras.utils import to_categorical
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from tensorflow.keras.preprocessing.text import *
from tensorflow.keras.preprocessing.sequence import pad_sequences
import pickle # pickle을 이용하면 int 값을 그대로 저장하고 그대로 불러옴
from konlpy.tag import Okt # 형태소 분리기 임포트
 
pd.set_option('display.unicode.east_asian_width',True)
 
df = pd.read_csv('/content/10x10(kor2).csv', index_col=0)
 
print(df.head())
print(df.info())
 
# 중복 행 확인
col_dup = df['title'].duplicated()
print(col_dup)
 
sum_dup = df.title.duplicated().sum()
print(sum_dup)
 
 
# title 컬럼 기준으로 중복 제거 (row 통째로 제거)
df = df.drop_duplicates(subset=['title'])
 
sum_dup = df.title.duplicated().sum()
print(sum_dup)
 
 
# 인덱스 새로 고침
df.reset_index(drop=True# False로 줄 시 기존 인덱스를 colum으로 올림
               inplace=True)
 
print(df.head())
print(df.tail())
cs

이 과정만 잘 따라와도 누구나 쉽게 카테고리 분류가 가능하다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 데이터 나누기
= df['title']
= df['category']
 
 
# 원핫인코딩 위해 라벨로 변경
encoder = LabelEncoder()
labeled_Y = encoder.fit_transform(Y)
label = encoder.classes_
 
# 지정한 라벨 저장
# 이미 학습한 라벨들을 이용해야 학습된 결과들과 똑같이 도출
with open('/content/10x10_category_encoder.pickle''wb'as f:
    pickle.dump(encoder, f)   
 
# 원핫인코딩 => 정답데이터 원핫인코딩(희소벡터처리)
onehot_Y = to_categorical(labeled_Y)
 
# 첫번째 데이터 형태소로 분리
okt = Okt()
okt_X = okt.morphs(X[0])
 
 
# 모든 row를 형태소로 분리
for i in range(len(X)):
    X[i] = okt.morphs(X[i])
cs

추후에 데이터들을 불러서 모델 성능이 얼마나 좋은지 평가를 하기 위해서 라벨들을 통일시켜야 한다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# stopwords 불러오기
stopwords = pd.read_csv('/content/stopwords2.csv')
 
# stopwords에 있는 단어를 제거하고 리스트에 담기
words = [] # 빈 리스트 생성
 
for word in okt_X:
    if word not in list(stopwords['stopword']):
        words.append(word)
 
#한 행씩 모든 데이터 처리
for i in range(len(X)): # X=df['title']
    result = []
    for j in range(len(X[i])):
        if len(X[i][j]) > 1:
            if X[i][j] not in list(stopwords['stopword']):
                result.append(X[i][j])
    X[i] = ' '.join(result)
cs

불용어 제거 단계

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#단어를 컴퓨터에서 인식시키기 위해 각 단어마다 숫자(라벨링) 부여 => 토큰화
# 수치적인 의미는 없음
token = Tokenizer()
token.fit_on_texts(X)
tokened_X = token.texts_to_sequences(X)
print(tokened_X[0])
 
#읽고 쓰는 과정에서 데이터의 자료형을 따로 인코딩할 필요 없이 그대로 저장
import pickle
 
with open('/content/10x10_token3.pickle''wb'as f:
    pickle.dump(token, f)
 
wordsize = len(token.word_index) + 1
 
max = 0
for i in range(len(tokened_X)):
    if max < len(tokened_X[i]):
        max = len(tokened_X[i])
 
#LSTM모델 사용을 위해 데이터 크기가 다른 것들을 0을 채워넣어 max사이즈로 통일시킴
X_pad = pad_sequences(tokened_X, max)
 
X_train, X_test, Y_train, Y_test = train_test_split(
    X_pad, onehot_Y, test_size=0.1)
print(X_train.shape)
print(X_test.shape)
print(Y_train.shape)
print(Y_test.shape)
 
xy = X_train, X_test, Y_train, Y_test
np.save('/content/10x10_data_max_{}_size_{}_2'.format(max, wordsize), xy)
cs

언어를 컴퓨터가 이해하기 위해서 토크나이징 과정을 거친다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import *
from keras.callbacks import EarlyStopping
 
 
X_train, X_test, Y_train, Y_test = np.load(
    '/content/datasets/10x10_data_max_13_size_20320_2.npy',
    allow_pickle=True)
#max: 한 행의(리스트 글자의) 최대 길이
#size: 차원 수
 
##모델 생성
model = Sequential()
#X_pad로 사이즈 통일시킨 데이터를 embedding layer가 원핫인코딩+벡터라이징 처리해줌
#벡터라이징: 각 인덱스별 의미부여를 벡터공간이라는 개념을 도입해 처리
 
model.add(Embedding(20320300, input_length=13))
#차원수 20320를 차원수 300으로 낮추고 input_length는 max최대값인 13으로 입력
 
model.add(Conv1D(512, kernel_size=5
            padding='same', activation='relu'))#1차원 컨볼루션=> conv1d, 2차원 컨볼루션=>conv2d
model.add(MaxPool1D(pool_size=1))
#conv다음에는 maxpool함께 감
 
model.add(LSTM(128, activation='tanh',
               return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(64, activation='tanh',
               return_sequences=True))
model.add(Dropout(0.1))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dense(10, activation='softmax'))#y값개수: 10(카테고리개수)
early_stopping = EarlyStopping(monitor='val_accuracy', patience=3)  
# 5 에포크 동안 해당 값이 좋아지지 않으면 학습을 중단.
 
print(model.summary())
cs

모델 구조

 

 

 

모델 정확도는 높게 나오지만 지속적으로 val_loss 값이 증가하는 것으로 보아, 약간의 과적합이 발생함을 확인했다.

 

제목 자체가 길지 않기 때문에 okt로 형태소 분석을 끝내고, stopwords로 불용어를 제거, 기타 전처리 과정을 끝냈을 때, 제목 최대 길이가 13밖에 나오지 않아서 그렇지 않을까 하는 추측을 해본다.

 

 

 

학습한 모델을 가지고 신상품순, 판매량순으로 새로 데이터를 크롤링해서 성능을 평가해보았다. 지속적으로 학습을 시키고, 더 많은 데이터를 학습시켰다면 신상품이 들어온다고 해도 정확도가 많이 떨어지지 않을 것으로 보인다.

 

 

 

 

 

10x10으로 학습시킨 모델을 쿠팡, 지마켓에 적용하려고 하니까 성능이 확실히 떨어지는 것을 확인할 수 있다. 제목도 각 사이트마다 짓는 방법도 다를 것이며, 같은 제품이라도 다른 카테고리에 있음을 확인했다.

 

예를 들어, 비타민C 고려은단은 당연히 건강식에 있을거라고 생각했는데... 쿠팡에서 아이 유아쪽 카테고리에 있었다.

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

두번째 프로젝트 순서

1. 프로젝트 주제 정하기

2. 기획 및 데이터 수집, 전처리

3. 데이터 저장(판다스 열/행 관련 정리)

4. 시각화 및 자동화


프로젝트 마무리

매 10분마다 크롤링을 진행하고, 19시가 되면 이슈와 사설을 이메일로 보내준다. 자동화는 코드가 어디있는지 주섬주섬 다시 찾아봐야 한다 ㅠㅠ. 진행을 이슈파트 따로 사설파트 따로 해서, 사설은 어떤 식으로 코드 진행이 됐는지 잘 모르겠지만 나쁘지 않은 결과가 나왔다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
import os
from datetime import datetime
 
base_dir = 'C:/Workspace/project2_final/output/'
os.makedirs(base_dir, exist_ok=True)
 
from wordcloud import WordCloud
wc = WordCloud(font_path=r'C:\Windows\Fonts\MalgunBD.ttf',background_color="white",
               max_words=150, max_font_size=300, width=800, height=800)
cloud = wc.generate_from_frequencies(dict(sum_search))
 
cloud.to_file(base_dir+'IssueKeyWord '+ datetime.today().strftime('%Y%m%d'+ '.png')
 
cs

하루 일정이 마무리 되면 실시간 TOP100개의 키워드와 언급된 게시물 조회수의 비중에 따라 워드클라우드를 생성한다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#### 상위 5개 키워드 ####
 
df = pd.read_excel(path2, header=[0,1], index_col=[0] )
df_col = list(df.columns.levels[0])
 
hit_top5 = []
 
for i in df_col :
    hit_max = df[i,'조회수'].max()
    hit_top5.append(hit_max)
 
hit_dict = dict(zip(df_col,hit_top5)) #딕셔너리 값으로 저장
 
keyword_top5 = sorted(hit_dict, key=hit_dict.get, reverse = True)[:5#탑 5개 keyword 추출
print(keyword_top5)
cs

 



 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#### 19시에 코드 보내기
 
import matplotlib.pyplot as plt
import seaborn as sns
 
title_font = {'fontsize'16'fontweight''bold'}
plt.rc('font',family='Malgun Gothic')
plt.figure(figsize=[20,10])
plt.style.use('ggplot')
plt.show(block=False)
plt.pause(1)
plt.close()
 
ymd = today.strftime('%Y-%m-%d'
hms = '19:00:00'
 
if hms == '19:00:00':
    path_png = "C:/Workspace/project2_final/output/graphs"
    if not os.path.isdir(path_png):                                                           
        os.mkdir(path_png)
    
    path_date = f"C:/Workspace/project2_final/output/graphs/{ymd}"
    if not os.path.isdir(path_date):                                                           
        os.mkdir(path_date)    
    
    ##### 그래프 조회수 변동 ######
    for i in keyword_top5 :
        plt.title (f"키워드 : '{i}' 조회수 변동", fontsize=20)
        df.index,df[i,'조회수'].plot( kind='bar')
        #    plt.bar(df.index,df[i,'조회수'])
        #plt.show()
        plt.savefig(f'{path_date}/조회수변동-({i}).png', bbox_inches='tight')
        plt.close()
                 #.plot( kind='bar')
 
    ##### 그래프 키워드 관심도 #####
    for i in keyword_top5 :
        text =f'{i} 키워드 관심도'
        sub_keys = df[i].groupby(['서브키워드'])['조회수'].mean().sort_values()
        plt.title(text, fontdict=title_font, loc='center', pad= 20)
        sub_keys.plot(kind='pie', autopct = '%1.1f%%', shadow = True, startangle=110 )
        #plt.show()
 
        plt.savefig(f'{path_date}/관심도-({i}).png')
        plt.close()
    
    print('그래프 시각화 완료')
cs

19시가 되면 조회수 변동, 키워드에 따른 서브 키워드가 얼마나 변했는지 확인할 수 있다. 이거는 강의실에 와서 발표전 30분 데이터를 취합해서 만든 것이라 서브 키워드 변동도 거의 없고, 조회수 변동폭도 크지 않음을 확인할 수 있다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import smtplib
from email.mime.multipart import MIMEMultipart;
# 메일의 본문 내용을 만드는 모듈
from email.mime.text import MIMEText;
# 메일의 첨부 파일을 base64 형식으로 변환
from email.mime.application import MIMEApplication;
# 메일의 이미지 파일을 base64 형식으로 변환(Content-ID 생성)
from email.mime.image import MIMEImage;
# 메일의 음악 파일을 base64 형식으로 변환(Content-ID 생성)
from email.mime.audio import MIMEAudio;
# 파일 IO
import io;
 
# 메일 서버와 통신하기 전에 메시지를 만든다.
data = MIMEMultipart();
# 송신자 설정
data['From'= "본인의 이메일";
# 수신자 설정 (복수는 콤마 구분이다.)
data['To'= "이메일1","이메일2";
# 메일 제목
data['Subject'= "제목"
with open("C:\\workspace\\project2_final\\mailsource\\hi.png"'rb'as fp:
    img = MIMEImage(fp.read(), Name = "hi.png")
    img.add_header('Content-ID''<hi>')
    data.attach(img)
 
with open("C:\\workspace\\project2_final\\mailsource\\issue.png"'rb'as fp:
    img = MIMEImage(fp.read(), Name = "issue.png")
    img.add_header('Content-ID''<issue>')
    data.attach(img)
    
with open("C:\\workspace\\project2_final\\mailsource\\news.png"'rb'as fp:
    img = MIMEImage(fp.read(), Name = "news.png")
    img.add_header('Content-ID''<news>')
    data.attach(img)    
    
with open("C:\\workspace\\project2_final\\mailsource\\tw.png"'rb'as fp:
    img = MIMEImage(fp.read(), Name = "tw.png")
    img.add_header('Content-ID''<tw>')
    data.attach(img)    
    
with open("C:\\workspace\\project2_final\\mailsource\\fb.png"'rb'as fp:
    img = MIMEImage(fp.read(), Name = "fb.png")
    img.add_header('Content-ID''<fb>')
    data.attach(img)    
 
with open("C:\\workspace\\project2_final\\output\\IssueKeyWord {}.png".format(datetime.today().strftime('%Y%m%d')), 'rb'as fp:
    img = MIMEImage(fp.read(), Name = "wc1.png")
    img.add_header('Content-ID''<wc1>')
    data.attach(img)
    
with open("C:\\workspace\\project2_final\\output\\KeyWord {}.png".format(datetime.today().strftime('%Y%m%d')), 'rb'as fp:
    img = MIMEImage(fp.read(), Name = "wc2.png")
    img.add_header('Content-ID''<wc2>')
    data.attach(img)
    
 
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# Html 형식의 본문 내용 (cid로 이미 첨부 파일을 링크했다.)
 
html = """
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>하루 이슈</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body style="margin: 0; padding: 0;">
 <table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border: 1px solid #cccccc;">
 <tr>
  <td align="center" bgcolor="#bdd7ee" style="padding: 40px 0 30px 0;">
 <img src="cid:hi" alt="Creating Email Magic" width="600" height="380" style="display: block;" />
</td>
 </tr>
 <tr>
  <td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
 <tr>
  <td font-family: 'Noto Sans KR', sans-serif;>
   <table border="0" cellpadding="0" cellspacing="0" width="100%">
 <tr>
  <td width="260" valign="top" font-family:Noto Sans KR, sans-serif; font-size: 16px; line-height: 20px;"> 
   <h2>커뮤니티 인기 키워드 100</h2>
   <img src="cid:wc1" alt="Creating Email Magic" width="250" height="250" style="display: block;" />
  </td>
  <td style="font-size: 0; line-height: 0;" width="20">
   &nbsp;
  </td>
  <td width="260" valign="top" font-family:Noto Sans KR, sans-serif; font-size: 16px; line-height: 20px;"> 
   <h2>사설 주요 키워드 100</h2>
   <img src="cid:wc2" alt="Creating Email Magic" width="250" height="250" style="display: block;" />
  </td>
 </tr>
</table>
  </td>
 </tr>
 <tr>
  <td style="padding: 20px 0 30px 0; font-family:Noto Sans KR, sans-serif; font-size: 16px; line-height: 20px;">
   <p>오늘 하루도 수고하셨습니다. 하이와 함께 행복한 하루를 마무리하세요!</p>
   <br>
   <br>
  </td>
 </tr>
 <tr>
  <td>
  <table border="0" cellpadding="0" cellspacing="0" width="100%">
 <tr>
  <td width="260" valign="top">
   <table border="0" cellpadding="0" cellspacing="0" width="100%">
    <tr>
     <td bgcolor="#3B89A3" align="center" style="padding: 20px 30px 20px 30px;">
      <img src="cid:issue" alt="" width="50%" height="100" style="display: block;" />
     </td>
    </tr>
    <tr>
     <td style="padding: 10px 0 30px 0; font-family:Noto Sans KR, sans-serif; font-size: 16px; line-height: 20px;"> 
     <h3 style="font-family:Noto Sans KR, sans-serif;">오늘의 커뮤니티 인기 키워드</h3>
      <details><p style="line-height:180%">{1}</p>
       <summary>{0}
       </summary>
      </details>
      <br>
      <details><p style="line-height:180%">{3}</p>
       <summary>{2}
       </summary>
      </details>
      <br>
      <details><p style="line-height:180%">{5}</p>
       <summary>{4}
       </summary>
      </details>
      <br>
      <details><p style="line-height:180%">{7}</p>
       <summary>{6}
       </summary>
      </details>
      <br>
      <details><p style="line-height:180%">{9}</p>
       <summary>{8}
       </summary>
      </details>
     </td>
    </tr>
   </table>
  </td>
  <td style="font-size: 0; line-height: 0;" width="20">
   &nbsp;
  </td>
  <td width="260" valign="top">
   <table border="0" cellpadding="0" cellspacing="0" width="100%">
    <tr>
     <td bgcolor="#3B89A3" align="center" style="padding: 20px 30px 20px 30px;">
      <img src="cid:news" alt="" width="50%" height="100" style="display: block;" />
     </td>
    </tr>
    <tr>
     <td style="padding: 10px 0 30px 0; font-family:Noto Sans KR, sans-serif; font-size: 16px; line-height: 20px;"> 
     <h3 style="font-family:Noto Sans KR, sans-serif;">오늘의 사설 주요 키워드</h3>
      <details><p style="line-height:180%">{11}</p>
       <summary>{10}
       </summary>
      </details>
      <br>
      <details><p style="line-height:180%">{13}</p>
       <summary>{12}
       </summary>
      </details>
      <br>
      <details><p style="line-height:180%">{15}</p>
       <summary>{14}
       </summary>
      </details>
      <br>
      <details><p style="line-height:180%">{16}</p>
       <summary>오늘 자주 등장한 키워드
       </summary>
      </details>
      
     </td>
    </tr>
   </table>
  </td>
 </tr>
</table>
  </td>
 </tr>
</table>
</td>
 </tr>
 <tr>
  <td bgcolor="#bdd7ee" style="padding: 30px 30px 30px 30px;">
 <table border="0" cellpadding="0" cellspacing="0" width="100%">
 <tr>
 <td width="75%"; style="color: #000000; font-family: Arial, sans-serif; font-size: 14px;">
 &reg; 하루이슈, Hi 2021<br/>
 <a href="" style="color: #000000;"><font color="#000000">Unsubscribe</font></a> to this newsletter instantly
</td>
  <td align="right">
 <table border="0" cellpadding="0" cellspacing="0">
  <tr>
   <td>
    <a href="http://www.twitter.com/">
     <img src="cid:tw" alt="Twitter" width="38" height="38" style="display: block;" border="0" />
    </a>
   </td>
   <td style="font-size: 0; line-height: 0;" width="20">&nbsp;</td>
   <td>
    <a href="http://www.facebook.com/">
     <img src="cid:fb" alt="Facebook" width="38" height="38" style="display: block;" border="0" />
    </a>
   </td>
  </tr>
 </table>
</td>
 </tr>
</table>
</td>
 </tr>
</table>
</body>
 
</html>
""".format(keyword_top5[0], articlestr1, keyword_top5[1], articlestr2, keyword_top5[2], articlestr3, keyword_top5[3], articlestr4, keyword_top5[4], articlestr5, sendingKeywords[0], content1, sendingKeywords[1], content2, sendingKeywords[2], content3, dfCSV["text"].tolist())
 
 
msg = MIMEText(html, 'html')
 
 
# 메시지를 확인한다.
# Data 영역의 메시지에 바운더리 추가
data.attach(msg);
print(data);
# 메일 서버와 telnet 통신 개시
server = smtplib.SMTP_SSL('smtp.naver.com',465);
#server = smtplib.SMTP('smtp.gmail.com',587);
# 메일 통신시 디버그
server.set_debuglevel(1);
# 헤로 한번 해주자.(의미 없음)
server.ehlo();
# tls 설정 주문 - tls 587 포트의 경우
#server.starttls();
# 헤로 또 해주자.(의미 없음)
server.ehlo();
# 로그인 한다.
server.login("ID", "P/W!"); # 아이디, 패스워드 입력
# 심심하니 또 헤로 해주자.(의미 없음)
server.ehlo();
# MAIL(송신자) 설정
sender = data['From'];
# RCPT(수신자), 리스트로 보낸다.
# 수신자 추가
receiver = data['To'].split(",");
# # 참조자 추가
# if data['Cc'] is not None:
#     receiver += data['Cc'].split(",");
# # 숨은 참조자 추가
# if data['Bcc'] is not None:
#     receiver += data['Bcc'].split(",");
# 메일 프로토콜 상 MAIL, RCPT, DATA 순으로 메시지를 보내야 하는데 이걸 sendmail함수에서 자동으로 해준다.
server.sendmail(sender, receiver, data.as_string());
# QUIT을 보내고 접속을 종료하고 메일을 보낸다.
server.quit();
 
cs

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

두번째 프로젝트 순서

1. 프로젝트 주제 정하기

2. 기획 및 데이터 수집, 전처리

3. 데이터 저장(판다스 열/행 관련 정리)

4. 시각화 및 자동화


각종 커뮤니티를 모두 크롤링하기에는 시간도 없고 벅차서, 이슈링크라는 싸이트에서 이미 친절하게 각종 커뮤니티를 크롤링해주고 있어서 이슈링크 싸이트를 이용하였다. 봇을 이용하여 글들을 긁어와주는 걸로 보인다. 

 

하지만 확인 결과, 오늘의 이슈태그 Top5커뮤니티 베스트 키워드들은 실제로 다수의 사람들이 관심이 있는 것이 아니었다. 분석하기로는 얼마나 커뮤니티에서 자주 언급되는지에 따라서 순위가 올라가는 것으로 보인다.

 

그래서 다수의 사람들이 관심이 있는 키워드와 이슈거리를 어떻게 하면 찾을 수 있을까 생각하면서 파이썬 프로젝트를 진행했다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#현재 시간 설정
from datetime import datetime, date, time, timedelta
 
now = datetime.now()
nowDatetime = now.strftime('%Y-%m-%d %H:%M')
print(nowDatetime)
 
 
#### 이슈 빼오기 #####
import requests
from bs4 import BeautifulSoup
 
url = 'https://www.issuelink.co.kr/community/listview/all/3/adj/_self/blank/blank/blank'
headers = {'user-agent''Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36'}
html_rank = requests.get(url, headers=headers).text
soup_rank = BeautifulSoup(html_rank, 'lxml')
keywords = soup_rank.select('div.ibox.float-e-margins > div > table > tbody > tr > td > a')
 
 
key_list = []
for k in keywords:
    keyword = k.text
    key_list.append(keyword)
 
###### 7시를 기준으로 기준 리스트를 하나 만들어야 함 / datetime에서 시간 분만 빼와서 if로 비교
 
key_list.pop(0)
print(key_list)
cs

 

 

 

예측을 한 것인데 실제로 확인을 해보니 총조회수가 높지 않지만 많이 언급될 수록 상위권에 있는 것으로 확인이 됐다. 이제 이 과정을 코드로 시행.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
### 이슈 실제 조회수 ###
 
from bs4 import BeautifulSoup
import requests
import urllib
import operator
 
sum_list = []
search_list = key_list
 
def sum(search) :
    
    url = f'https://www.issuelink.co.kr/community/listview/read/3/adj/_self/blank/{search}'
    headers = {'user-agent''Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36'}
    r = requests.get(url, headers=headers)
    soup = BeautifulSoup(r.text, 'html.parser')
 
    hits = soup.select('span.hit')
    
    sum=0
    
    for hit in hits :
        sum += int(hit.text.replace(',',''))
    
    sum_list.append(sum)  
    print('*',end='')
    
for search in search_list :
    sum(search)
 
#조회수, 키워드 합치기
sum_search = dict(zip(key_list,sum_list))
 
#조회수 순으로 정렬
#a = sorted(sum_search.items(), key=lambda x:x[1], reverse = True)
 
print()
print(sum_search)
cs

이슈가 되는 키워드를 검색했을 때, 각 커뮤니티별로 얼만큼의 조회수를 보이고 있는지 확인을 했다. 문제는 각 사람들마다 성향이 있어서 특정 싸이트를 보여주면, 극도로 싫어하는 사람들이 있다. 그래서 커뮤니티 글 중에 조회수가 높은 글을 보여주기에는 애매해서, 이를 네이버 뉴스로 보여주려고 했다.

 

다만 네이버 뉴스를 예로 들었을 때, 당시 '브레이브걸스' 로션은 알테니 스킵을 아재 팬들이 몰라서 이슈가 됐었다. 하지만 당시 브레이브걸스 자체가 가지고 있는 인기가 있어서, 브레이브걸스를 뉴스에서 검색해도 이 알테니 스킵이라는 이슈와는 다른 이슈가 검색이 될 수도 있다. 그래서 서브키워드를 같이 추출하기 위해서 형태소 분석을 했다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#### 키워드 제목 추출 탐30 개별로 ####
 
from bs4 import BeautifulSoup
import requests
import urllib
import os
 
#디렉토리 폴더 생성
path = "./subject"
if not os.path.isdir(path):                                                           
    os.mkdir(path)
 
keyword_list=[]
 
def subject(search) :
    url = f'https://www.issuelink.co.kr/community/listview/all/3/adj/_self/blank/{search}'
    headers = {'user-agent''Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36'}
    r = requests.get(url, headers=headers)
    soup = BeautifulSoup(r.text, 'html.parser')
 
    sub = soup.select('span.title')
    
    keyword_list.clear()
    
    for i in sub :
        split_string = i.get_text().split(' [',1)
        substring = split_string[0]    
        keyword_list.append(substring)
        
    with open(f'./subject/{search}.txt','w', encoding = 'utf-8'as file :
        file.writelines(keyword_list)
    
    print('**', end="")
            
    
for search in search_list :
    subject(search)
 
print()
print('완료')
cs

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#### 키워드 형태소 카운팅 ####
 
""" 형태소 분석기
    명사 추출 및 빈도수 체크
    python [모듈 이름] [텍스트 파일명.txt] [결과파일명.txt]
"""
 
import sys
from konlpy.tag import Twitter
from collections import Counter
 
 
def get_tags(text, ntags=50):
    spliter = Twitter()
    nouns = spliter.nouns(text)
    count = Counter(nouns)
    return_list = []
    for n, c in count.most_common(ntags):
        temp = {'tag': n, 'count': c}
        return_list.append(temp)
    return return_list
 
 
def main(search):
    # 분석할 파일
    noun_count = 50
    # count.txt 에 저장
    open_text_file = open(f'./subject/{search}.txt''r',-1,"utf-8")
    # 분석할 파일을 open 
    text = open_text_file.read() #파일을 읽습니다.
    tags = get_tags(text, noun_count) # get_tags 함수 실행
    open_text_file.close()   #파일 close
    open_output_file = open(f"./subject/{search}-count.txt"'w',-1,"utf-8")
    # 결과로 쓰일 count.txt 열기
    for tag in tags:
        noun = tag['tag']
        count = tag['count']
        open_output_file.write('{} {}\n'.format(noun, count))
    # 결과 저장
    open_output_file.close() 
 
for search in search_list :
    main(search)
    
print('완료')
 
cs

당시에 형태소 분석을 제대로 다루지 못해서 구글을 통해서 검색을 했다. 그리고 제목보다는 서브키워드가 중요하다고 생각하여서, 제목은 잠시 파일에 저장하고 시간이 지나면 삭제하는 식의 과정을 진행했다.

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
from bs4 import BeautifulSoup
 
sub_key = {}
 
for i in sum_search :
      
    with open(f'C:/Workspace/project2_final/output/temp/{i}-count.txt','r', encoding = 'utf-8'as file :
        data = str(file.readlines()[1])
 
    split_string = data.split(' ',1
    substring = split_string[0]           #빈도수 제거 
    #print(substring)
    
    sub_key[i] = substring
    
    
print(sub_key)
cs

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#네이버 검색, 키워드 서브키워드 
import requests
from bs4 import BeautifulSoup
 
art_lists = []
 
def search(key, b) :
    
    art_list = [b]
    
    #url = f'https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=1&ie=utf8&query={key}'
    url = f'https://search.naver.com/search.naver?where=news&sm=tab_jum&query={key}'
    html = requests.get(url).text
    soup = BeautifulSoup(html, 'lxml')
    
    for i in range(2) :
        news = soup.select('div.info_group > a:nth-of-type(2)')[i].attrs["href"]
        art_list.append(news)
    
    art_lists.append(art_list)
    
= sorted(sum_search.items(), key=lambda x:x[1], reverse = True#value 값 기준으로 정렬, 상위 5개 키워드
 
 
for i in range(5) :
    b= a[i][0]     #정렬 후 dic -> list 함수로 변환돼서 [i][0]으로 빼옴 
    #print(b)
    
    with open(f'C:/Workspace/project2_final/output/temp/{b}-count.txt','r', encoding = 'utf-8'as file :
        data = str(file.readlines()[1])
 
    split_string = data.split(' ',1
    substring = split_string[0]           #빈도수 제거 
    #print(substring)
    
    key = b + " " + substring
    search(key, b)
    
print(art_lists)
print(sum_search.items())
 
cs

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

두번째 프로젝트 순서

1. 프로젝트 주제 정하기

2. 기획 및 데이터 수집, 전처리

3. 데이터 저장(판다스 열/행 관련 정리)

4. 시각화 및 자동화

 


 

 

주제 : 하루 동안 있었던 이슈와 볼거리 정리

일정 : 4/29~5/7 (거의 5월 프로젝트인데 이제서야 정리를 하네요...)

기획 : 가십거리와 함께 전문적인 의견들에 대해서 하루를 정리하는 시간에 받아 보는 서비스

 

 

 

출처 : 조선일보

[네이버 실시간 검색어 폐지]

네이버 실시간 검색어도 갑자기 폐지됐다. 실검에 자꾸 광고성 키워드들이 올라와서 실검을 폐지했다는데 황당하다. 상품 키워드들만 올라가는 창을 만들든 트래픽을 분석해서 억제를 시키면 되는데, 소비자들의 편의성을 없앴다. 솔직히 실검보려고 네이버를 사용하는 사람들이 많았을텐데 얼마나 영향을 끼쳤을지 궁금하다.

 

(네이버는 이전에도 홈 화면을 뉴스나 실검이 나오는 창이 아닌 구글을 따라하듯이 바꿨는데, 이 막대한 손해는 어떻게 극복했는지도 궁금하다. 네이버 블로그도 바이럴 마케팅에 먹힌지 너무 오래 됐고, 인플루언서도 팔로워를 돈으로 사는 마당에 신뢰도를 어떻게 회복할지도 궁금한데.. 이건 뭐 뻘소리라서 패스..)

 

 

 

 

[이슈 생성 과정]

있다가 없어지면 불편하기 마련, 그래서 사람들이 어떤 것에 관심을 가지고 있고,

어떤 사건이 일어나는지 이슈의 생성 과정을 분석해보았다.

 

1. SNS 또는 커뮤니티에서 먼저 사건이 커진다. 

2. 그것이 커뮤니티로 돌고 돌아 공유가 돼서 커뮤니티에 상주하는 기자들이 기사를 쓰기 시작한다.

 (기사가 화제가 되는 경우도 있다.)

3. 기사를 본 사람들이 관련 내용이 어떤지 궁금해서 찾아보게 된다.

4. 실시간 검색어 순위에 오르게 되고, 기사나 관련 내용을 모르는 사람들은 실검을 클릭하게 된다.

 

=> 이러한 점을 미루어 봤을 때 커뮤니티에서 어떤 것이 이슈가 되고 있는지 파악해서 이슈를 예측해본다.

 

 

 

파이썬 업무 자동화를 배워서 이번에는 자동으로 시스템이 돌아가고, 자동으로 메일을 보내는 시스템을 구현하려고 한다. 원래는 Django를 통해서 웹 서비스를 구현해보려고 했는데, 생각보다 시간이 많이 걸려서 다음에 해보기로 했다.

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

- 첫 프로젝트 글 순서 -
1. 파이썬(python) EDA 데이터분석 주제 정하기
2. 실패한 여기어때 후기 웹스크래핑(web scraping)
3. 데이터 수집 방법 & 데이터 추출, 정제
4. 판다스(pandas) 데이터 처리 / Matplotlib, Json 시각화
5. 정리

지난 번에 글을 올린지 딱 2달이 지났다. 이후에 프로젝트를 벌써 3개를 더했는데... 틈만 나면 해야지 해야지 하는데 시간이 많이 나지를 않았다. 머신러닝, 인공지능 등 개념학습 따라가기도 너무 벅찼고, 모델은 아직까지 이해도 안 되는 수준이긴 하지만 더 늦어지면 안 될 것 같아서 미리미리 간단하게 쓰기로 했다.

솔직히 EDA 데이터 분석 팀프로젝트 5번째 항목 정리를 왜 했지라는 생각이 든다. 그래서 간단하게 어떻게 진행을 했고 어떤 것을 사용했는지, 결론은 어떻게 도출했는지만 간단하게 쓰려고 한다. 부지런하게 남은 프로젝트들도 업로드하면서 github도 사용해봐야 하는데 쉽지 않다. 개인적으로 마케팅 분석하고 sns까지 하다보니 시간이 남지 않는다. 해커톤까지 ???

나중에 알게 된건데 필수로 써야하는 부분이 몇개 있다.

1. 언어(파이썬, 자바, C 등..), 패키지(matplolib, pandas 등), 툴(주피터노트북, 코랩, 파이참 등..)
2. 일정표
3. 과정, 순서 등





이번에 주로 사용했던 것은 pandas, matplolib, json을 사용하였다. sns도 사용하려고 했는데 당시에는 생각보다 쉽지 않아서 matplotlib으로 데이터 시각화를 대부분 담당했다. 나중에도 시각화하는데 많이 사용하기 때문에 알아두는 것이 좋다.


과정, 순서 등은 어떤 부분에서 얼만큼의 시간을 썼는지 쓰면 좋을 것이다.


프로젝트를 할 때 주제가 많이 제한적이게 된 이유는 데이터를 구하기 너무 어렵다. 예전에 데이터가 곧 권력이고 힘이라고 했던 부분, 그리고 스프링쿨러? 온도, 습도 등의 데이터를 모아놨던 회사가 구글에 데이터를 고가에 팔았다는 것, 머신러닝을 하면서 더 느끼게 됐다.


데이터를 토대로 결론 도출을 해봤다
- 창업시 고려해야할 위치와 업종을 데이터를 통해 파악하기 쉽다.
- 업종에서는 소매업이 가장 많고, 그 다음이 음식점, 그리고 도민의 소매업이 평상시에도 많다.
- 성수기 때 슈퍼마켓, 체인화 편의점 이용률이 많이 늘어났다. 숙박업을 할 경우 미리 물품 구매를 하면 더 좋을 것이다.

=> 하고 싶은 업종을 입력했을 때, 위치, 경쟁, 타겟층을 보여주는 프로그램을 보여줘도 나쁘지 않을 것 같다.

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

- 첫 프로젝트 글 순서 -

 

1. 파이썬(python) EDA 데이터분석 주제 정하기 

2. 실패한 여기어때 후기 웹스크래핑(web scraping)

3. 데이터 수집 방법 & 데이터 추출, 정제

4. 판다스(pandas) 데이터 처리 / Matplotlib, Json 시각화

5. 정리


데이터를 어떤 식으로 그룹핑을 할지 고민을 하다가 cost(비용)으로 하기로 했다. 

 

-큰 틀에서 묶기-

 

제주시/서귀포시 매출 비교

업종별 매출

성별/연령별 소비

개별/단체 소비

성수기/비수기

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 월별 / 남성 / 나이/ 종류 별 금액
sum_stores =  df.groupby(['date','sex','age','stores'])['cost'].sum()
print(sum_stores.plot(kind='bar', rot=90) )
 
#성수기 비수기
sum_sb = df.groupby('date')['cost'].sum()
 
 
#업종별 매출 
sum_stores = df.groupby('stores')['cost'].sum()
 
 
#성별, 연령별 소비 순위
sum_sa = df.groupby(['sex','age'])['cost'].sum()
sum_sa
 
#성별 업종 이용현황, 연령별 업종 이용현황, 성별&연령별 업종 이용현황
sum_st = df.groupby(['sex','stores'])['cost'].sum()
sum_at = df.groupby(['age','stores'])['cost'].sum()
sum_sat = df.groupby(['sex','age','stores'])['cost'].sum()
 
#제주시 서귀포시 소비 비교 / 읍면동별 소비 비교
sum_city = df.groupby(['city'])['cost'].sum()
sum_city2 = df.groupby(['city2'])['cost'].sum()
 
#제주시 서귀포시 업종별 소비 비교 / 읍면동 업종별 소비 비교
sum_ct = df.groupby(['city','stores'])['cost'].sum()
sum_ct2 = df.groupby(['city2','stores'])['cost'].sum()
 
#지역 각 성별 업종 이용률
sum_cst2 = df.groupby(['city2','sex','stores'])['cost'].sum()
 
#개별과 단체 총금액 비교
sum_visitor = df.groupby(['visitor'])['cost'].sum()
 
#개별 단체 성별 총금액 비교 / 개별 단체 업종별 총금액 비교 / 단체 or 개별에서 이용자별 금액비교
sum_vs = df.groupby(['visitor, sex'])['cost'].sum()
sum_vt = df.groupby(['visitor, stores'])['cost'].sum()
sum_vu = df.groupby(['visitor, user'])['cost'].sum()
 
#개별(내국인, 법인, 외국인) / 단체에서 이용하는 업종
sum_vut = df.groupby(['visitor, user, stores'])['cost'].sum()
cs

우선 데이터가 어떻게 보여지는지 기본적인 시각화를 진행했다. 만약 세부정보가 필요하면, 세분화해서 EDA데이터 분석하는데 유용하게 활용했다.

 

 

1
2
3
4
5
6
#특정 업종, 날짜
sum_sdt = df[(df.stores == '소매업')  & (df.date == 'Feb-17')].groupby('type')['cost'].sum()
 
#특정 연령, 
sum_ast = df[df.age==10].groupby(['sex','type'])['cost'].sum().sort_values().tail(20)
 
cs

열에서 특정 연령, 성별, 업종만 뽑아내서 더하고 싶으면, df[df.열 == '열 속성'].groupby를 이용해주면 된다. 특정 열에서 속성을 추출해서 더하는 코드가 나오지 않아서 참고하면 좋을 것 같다.

 

 

 

1
2
3
4
5
plt.rc('font',family='Malgun Gothic')
plt.figure(figsize=[20,10])
plt.style.use('ggplot')
sum_sa = df.groupby(['sex','age'])['cost'].sum()
sum_sa.plot(kind='bar'
cs

matplotlib 함수를 활용하여 가장 기본적인 시각화를 했다. 

 

 


 

얘네를 모아서 EDA 데이터 분석에 필요한 자료만 모으고, title, x축, y축, 그래프 색, 그래프 종류(box, bar, barh, violinplot, pie) 등으로 다시 시각화 과정을 진행했다.

 

 

 

1
2
3
4
5
6
7
8
9
10
plt.figure(figsize=[10,5])
title_font = {'fontsize'16'fontweight''bold'}
plt.title('도민 소매업 매출 순위', fontdict = title_font, loc = 'center', pad =20)
sum = df[(df.stores=='소매업'& (df.visitor=='도민')].groupby('type')['cost'].sum()
ax = plt.subplot()
ax.set_xticklabels(['0','500억''1000억''1500억''2000억''2500억''800억'])
sum.plot(kind = 'barh', color = '#f39189')
plt.xlabel('Total Amount')
plt.ylabel('Type of Business')
sum_sa
cs

barh 막대 그래프

 

 

 

1
2
3
4
5
6
explode = [ 0.10.1,0.10.1,0.1,0,0 ]
title_font = {'fontsize'16'fontweight''bold'}
plt.title('업종별 매출', fontdict=title_font, loc='center', pad= 20)
sum_sa = df.groupby('stores')['cost'].sum().sort_values()
sum_sa.plot(kind='pie',y='sum', autopct = '%1.1f%%',explode=explode, startangle=0)
 
cs

 

pie 그래프

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import pandas as pd
from pandas import Series, DataFrame
import matplotlib.pyplot as plt
import folium
from folium import plugins
from folium.plugins import MarkerCluster
import json
 
df1= pd.read_csv("jeju_card.csv",engine='python', encoding = 'euc-kr', header=0,
                names=['date','city''city2''type''user''visitor''age''sex''cost''stores'])
         # 카드 이용금액 csv
df2 = pd.read_csv("jeju_lalo.csv",engine='python', encoding = 'euc-kr')
#df2 = 제주도의 행정구역을 위도 및 경도로 나눈 csv
df3 = pd.read_csv("jeju_cost.csv",engine='python', encoding = 'euc-kr')
#df3 = 구역별 소비를 나타낸 csv
df = pd.DataFrame(df1)
 
lat = df2['위도'].mean()
long = df2['경도'].mean()
= folium.Map([lat,long], zoom_start=9)
# lay, long의 평균값으로 지도의 첫 화면 지정 / 줌lv = 9
= df.groupby(['city2'],  as_index = False)['cost'].sum()
# city2 컬럼을 그룹바이하고 그 중의 cost컬럼을 모두 sum.
# as_index = False : 그룹 바이 한 데이터 프레임에 index 번호를 추가해준다.
 
#---------------------------제이슨 표시---------------------------
#제주도의 행정구역을 시각화로 나눠주는 geojson road
with open("Jeju_json3.geojson", mode='rt',encoding='utf-8'as f:
    jeju_json = json.loads(f.read())
    f.close()
folium.GeoJson(jeju_json, name='jeju_haejoeng').add_to(m)
#------------------------------------------------------------------
 
#---------------------------제이슨 조건---------------------------
folium.Choropleth(geo_data = jeju_json,         # geo_data load
                  data = df3,                 # 위치를 표현해줄 데이터 load
                  columns = ('읍면동''이용금액'),     # ('지도에 표현할 컬럼', '비교할 컬럼')
                  key_on = 'feature.properties.adm_nm',     # geo_data load 후 데이터를 어디서 받을지
                  fill_color = 'YlGn',             # 비교해서 나타낼 색 / 노란색/초록색
                  fill_opacity = 0.8,             #
                  line_opacity = 0.5,             #
                  legend_name = '지역별 매출액'         # 우측 상단에 나오는 bar 이름
                  ).add_to(m)
#------------------------------------------------------------------
 
marker_cluster = MarkerCluster().add_to(m)         # 마커 클러스터 추가 (현재 코드엔 사용 안함.)
for i in range(len(a.index)):
    b = df2[df2['읍면동'== a.iloc[i,0]]         # df2 읍면동 컬럼에서 a의 [i,0] 값과 같은것을 추출
    sub_lat = b.iloc[0,4]                 
    sub_long = b.iloc[0,5]                 # 위, 경도 설정
    title = b.iloc[0,2]                 # title 설정
 
    folium.Marker([sub_lat,sub_long],tooltip = title).add_to(m) 
    # 마커 설정 마커클러스터를 실행시키려면 add_to(marker_cluster)
 
#folium.LayerControl(collapsed=False).add_to(m)     # 그룹화된 기능들을 사용하기위해 컨트롤러 설정
m.save('jeju.folium.html')
 
 
cs

제주도 marker

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import pandas as pd
from pandas import Series, DataFrame
import matplotlib.pyplot as plt
import folium
from folium import plugins
from folium.plugins import MarkerCluster
import json
 
# json 도시별 행정구역 구분 sggnm
df1= pd.read_csv(r"C:\Users\rhdud\Desktop\py_team\read_file\123123.csv",engine='python', encoding = 'euc-kr', header=0,
                names=['date','city''city2''type''user''visitor''age''sex''cost''stores'])
df2 = pd.read_csv(r"C:\Users\rhdud\Desktop\py_team\read_file\jeju_lalo.csv",engine='python', encoding = 'euc-kr')
#df2 = 제주도 행정구역 위도 및 경도로 나눈 csv
df3 = pd.read_csv(r"C:\Users\rhdud\Desktop\py_team\read_file\jeju_cost.csv",engine='python', encoding = 'euc-kr')
df = pd.DataFrame(df1)
 
 
lat = df2['위도'].mean()
long = df2['경도'].mean()
= folium.Map([lat,long],zoom_start=9)
# lay, long의 평균값으로 지도의 첫 화면 지정 / 줌lv = 9
= df.groupby(['city2','sex','stores'],  as_index = False)['cost'].sum()
'''
#---------------------------폴리움 그룹화---------------------------
fg = folium.FeatureGroup(name = '도시별 표현')
m.add_child(fg)
g1 = plugins.FeatureGroupSubGroup(fg, '제주시')
m.add_child(g1)
g2 = plugins.FeatureGroupSubGroup(fg, '서귀포시')
m.add_child(g2)
g3 = plugins.FeatureGroupSubGroup(fg, 'Json으로 표현')
m.add_child(g3)
#------------------------------------------------------------------
'''
#---------------------------제이슨 표시---------------------------
with open(r"C:\Users\rhdud\Desktop\py_team\read_file\Jeju_json.geojson", mode='rt',encoding='utf-8'as f:
    jeju_json = json.loads(f.read())
    f.close()
folium.GeoJson(jeju_json, name='jeju_haejoeng').add_to(m)
#------------------------------------------------------------------
 
#---------------------------제이슨 조건---------------------------
key_by = 'feature.properties.adm_nm'
folium.Choropleth(geo_data = jeju_json,
                  data = df3,
                  columns = ['읍면동''이용금액'],
                  key_on = key_by[key_by.rfind(' ')+1 :],
                  fill_color='YlGn',
                  fill_opacity=0.8
                  ).add_to(m)
#------------------------------------------------------------------
 
m.save('jeju_data1.html')
cs

제주도 choropleth

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

- 첫 프로젝트 글 순서 -

1. 파이썬(python) EDA 데이터분석 주제 정하기 

2. 실패한 여기어때 후기 웹스크래핑(web scraping)

3. 데이터 수집 방법 & 데이터 추출, 정제

4. 판다스(pandas) 데이터 처리 / Matplotlib, Json 시각화

5. 정리




웹스크래핑 vs 웹크롤링 

 

웹스크래핑 : 분석을 위해서 특정 데이트를 추출해서 자신이 원하는 형태로 가공한다.

웹크롤링 : 웹사이트의 데이터를 수집하는 모든 작업, 일종의 로봇이라고 생각하면 된다.

 

 

 

 

출처 : 위키백과

파이썬 EDA 데이터분석 프로젝트 과정

 

목표 : 소비자가 원하는 서비스 파악하기

위치 : 제주도 제주시

업태 : 게스트하우스

수집항목 : 이름, 위치, 가격, 평점, 후기, 기본정보

웹스크레이핑 방법 : 3~5개 숙박 업체 제주도 검색 -> 인기순 -> 100개대략적 위치, 후기 내용 추출 -> 숙소의 위치를 표시하고, 숙박업 어떤 것이 서비스가 필요한지 키워드 10~50위 순위 보여주기 (제외할 키워드 생각)

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import requests
from bs4 import BeautifulSoup 
import re
 
data = requests.get('https://www.goodchoice.kr/product/result?sort=ACCURACY&keyword=%EC%A0%9C%EC%A3%BC%EC%8B%9C&type=&sel_date=2021-04-08&sel_date2=2021-04-09&adcno%5B%5D=6&min_price=&max_price=')
name_list =[] #업소명
rate_list=[] #평점
price_list=[] #가격
 
html = data.text
soup = BeautifulSoup(html, 'html.parser')
n_list = soup.select('div.name  strong')
r_list = soup.select('p.score em')
p_list = soup.select('div.map_html b')
 
for i in n_list:
    name = i.string
    name_list.append((name).lstrip().replace("\t","").replace("\n","")) #탭이랑 줄바꿈 
    
for i in r_list:
    rate = i.string
    rate_list.append(rate)
    
for i in p_list:
    price = i.string
    price_list.append(price)
 
#print(rank_list)
#print(music_title)
#print(music_artist)
 
for i  in range(len(name_list)) :
    print('업소명:' , name_list[i])
    print('평점:' , rate_list[i])
    print('가격:' , price_list[i])
    print('-------------------------------')
 
cs

여기어때 사이트에 들어갔는데 데이터가 너무 방대해서, 제주시와 게스트하우스로 한정을 지었다. 우선 후기가 따로 들어가 있어서 일단 업소명, 평점, 가격을 추출하고 그 다음에 후기를 하나씩 추출하는 방식으로 진행을 하려고 했다.

 

 

BeautifulSoup : HTML과 XML 파일로부터 데이터를 가져오기 위한 라이브러리

find : html tag를 통한 크롤링

select : css를 통한 크롤링

find, select_one : 첫 번째 태그를 찾음

find_all, select : 모든 태그를 찾음

 

find보다 select가 '>'로 하위 태그로 접근이 쉬워서 여기어때 사이트에서 F12를 누르고 Ctrl + Shift + C 를 눌러서 element를 찾아서 업소명, 평점, 가격 별로 추출을 했다. 

 

 

 

 

제목 스크래핑하기 제목을 커서위에 대보니까, div class name > strong에 포함되어 있는 것을 확인할 수 있다. 하지만 출력을 해보면 줄바꿈과 탭으로 인해서 깔끔해보이지 않아서 replace()를 한 것이다. 중간중간 어떻게 출력이 되는지 print(n_list)를 각각 해보는 것을 추천한다.

 

 

 

 

 

결과를 확인해보니까 잘 출력되고 있다. 일정 부분까지 가고 스크롤이 내려가지 않아서 마지막에 에러가 뜨는 것으로 보인다. 

 

 

 

한계 1

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from time import sleep
from bs4 import BeautifulSoup as bs
from selenium import webdriver
from selenium.webdriver.common.keys import Keys #텍스트 외에 특수 키를 입력하기 위한 모듈
 
driver = webdriver.Chrome('chromedriver.exe')
url = "https://www.goodchoice.kr/"
driver.implicitly_wait(5)
driver.get(url)
 
search = driver.find_element_by_xpath('/html/body/div[1]/header/section/button[2]')
search.click()
#search.clear()
search = driver.find_element_by_xpath('//*[@id="keyword"]')
driver.implicitly_wait(5)
search.send_keys("제주시")
search.send_keys(Keys.ENTER)
#driver.implicitly_wait(5)
driver.set_window_size(1400,1000)
search = driver.find_element_by_xpath('//*[@id="content"]/div[1]/section[2]/ul/li[6]/label')
search.click()
search = driver.find_element_by_xpath('//*[@id="content"]/div[1]/div[2]/button[2]')
search.click()
cs

 

여기어때 싸이트에 들어감 -> 검색 창을 한 번 누르고, 한 번 더 눌러야함을 알 수 있음 -> 제주시를 입력하고, 엔터를 눌러서 검색 -> (gif에는 안 나오지만) 밑에 게스트하우스 클릭박스를 선택 후, 적용 클릭

 

우선 여기까지 구현을 하고 나서 게스트하우스를 하나씩 클릭하고 돌아오고는 할 수 있는데, 배운 선에서 웹 스크래핑으로 열린 창에 새로운 url 주소를 얻는 방법을 몰라서 벽에 부딪혔다 ㅠㅠㅠ

 

이건 나중에 개발자 친구한테 알아내서 추가하겠습니다.

 

 

 

한계 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from wordcloud import WordCloud
from konlpy.tag import Twitter
from collections import Counter
 
# open으로 txt파일을 열고 read()를 이용하여 읽는다.
text = open('news.txt', encoding='utf-8').read() 
twitter = Twitter()
 
# twitter함수를 통해 읽어들인 내용의 형태소를 분석한다.
sentences_tag = []
sentences_tag = twitter.pos(text) 
noun_adj_list = []
 
# tag가 명사이거나 형용사인 단어들만 noun_adj_list에 넣어준다.
for word, tag in sentences_tag:
    if tag in ['Noun' , 'Adjective']: 
        noun_adj_list.append(word)
 
# 가장 많이 나온 단어부터 40개를 저장한다.
counts = Counter(noun_adj_list)
tags = counts.most_common(40
 
 
# WordCloud를 생성한다.
# 한글을 분석하기위해 font를 한글로 지정해주어야 된다. macOS는 .otf , window는 .ttf 파일의 위치를
# 지정해준다. (ex. '/Font/GodoM.otf')
wc = WordCloud(font_path='c:\Window\Fonts\malgun.ttf' ,background_color="white", max_font_size=60)
cloud = wc.generate_from_frequencies(dict(tags))
 
import matplotlib.pyplot as plt
plt.figure(figsize=(108))
plt.axis('off')
plt.imshow(cloud)
# plt.to_file('test.jpg') test.jpg 파일로 저장
plt.show()
cs

수업시간에 잠깐 배웠던 내용을 가지고 후기에 어떤 키워드가 많이 사용되는지 확인하려고 했다. wordcloud를 생성해서 후기를 모으고 상위 단어 top50 개를 추리려고 했다. 하지만 웹 스크래핑도 안 될뿐만 아니라, 예외 단어들을 어떻게 정할지 하나하나 생각해야 했다. 사람들이 쓰는 방법들이 다 달라서 좋다, 좋았어요, 좋구만, 좋아요 다 다르게 인식하면 어쩌나하고 강사님과 상의 후에 다른 방식으로 진행하기로 했다.

 

 

 

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

파이썬 EDA 데이터분석 팀 프로젝트, 마케팅 관점에서 생각하기 (제주도)

 

- 첫 프로젝트 글 순서 -

1. 파이썬(python) EDA 데이터분석 주제 정하기 

2. 실패한 여기어때 후기 웹스크래핑(web scraping)

3. 데이터 수집 방법 & 데이터 추출, 정제

4. 판다스(pandas) 데이터 처리 / Matplotlib, Json 시각화

5. 정리

 

 

3월 중순에 교육을 시작해서 단 몇 주만에 파이썬을 배웠다. 6개월 뒤에 이 글을 다시 보게 되면 참 답답하게 코딩을 했구나라고 생각을 했으면 좋겠다는 심정으로.. 열심히 코딩을 할 생각이다.

 

* 코딩 부분은 다음 편부터 나옵니다. 일련의 과정이니 참고만 해주세요


EDA 란?

 

데이터를 수집하고 수집된 데이터를 다양한 각도에서 관찰하고 이해하는 과정으로 보면 된다. 데이터를 분석하기 전에 그래프나 통계적인(matplotlip, seaborn) 방법으로 시각화해, 더욱 직관적으로 데이터를 처리해서 분석한다.

 

 

 

출처 : blog.naver.com/hrd04/221135900560

 

이렇게 복잡한 과정을 거쳐야할 수도 있지만... 간단하게 보면

데이터 수집 => 시각화 탐색 => 패턴 도출 => 인사이트 발견 순으로 보면 좀 할만하네?라고 생각이 든다. 

 

 

 

 

주제 선정

 

모르면 일단 친구한테 물어보는게 마음 편하다. 생각보다 간단한 주제를 해서 별거 아닐거라고 생각했는데 큰 오산...

 

 

 

 

[마케팅적인 관점으로 접근] 

1. 검색수, 관심, 날씨 등의 키워드 분석으로 6개월 후에 팔릴 제품 예상

- 날씨는 기상청 공공데이터, 검색 수는 구글 트렌드, 네이버 datalab으로 어느 정도 데이터를 뽑을 수 있지만, 개인의 관심과 개인이 구매했던 제품들을 보려면 개인 정보가 필요한데 데이터를 구하기 힘들 것으로 판단해서 포기

 

 

 

 

 

 

2. 드라마에 나오는 제품(협찬)이 실제 검색량과 구매에 얼마나 영향을 미치는지

- 커뮤니티에 홍보글들이 올라와서, 조회수 당 실질적으로 구매에 얼마나 많은 영향을 미칠까.. 인플루언서 마케팅의 효율이 얼마나 될지를 객관적인 데이터 자료로 생각을 하려고 했지만, 이것 또한 구매까지 파악이 안 돼서 pass 

 

3. 네이버 view 상위 검색 분석 

- 네이버에도 알고리즘이 있다고 하지만 어느정도 상관관계가 있을 수 있으니 for 구문으로 지역별 + 맛집 돌려서 상위 5~10개의 글에서 키워드, 그림 개수, 영상 길이, 블로그 전체 글 수 등을 웹 스크래핑으로 통계를 내서 진행하려고 했으나... 말로는 쉽지 실제 하기는 어려웠다.

 

 

 

[팀 회의]

- 지역별, 성별, 연령 등으로 정리 할 수 있는 코로나 감염자 현황

- 서울 지하철 역 별로 승/하차 인원 및 현재 열차의 위치

- 서귀포시 와 제주시로 나누어 토지 비교 / 상업용 비교

- 연도별 토지값 변동, 각 동별로 차이가 있는가

- 동일 생활권내 각동별로 같은가

 

 

 

[EDA 과정]

간단한 주제 밖에 나오지 않아서 '제주도 예비 창업자를 위한 업종 서비스 추천'이라는 EDA 팀 프로젝트 주제를 정했다 => 데이터를 찾아보니 너무 주제가 광범위해서, 숙박업으로 좁혔다(숙박업 서비스 개선을 위한 방안) => 그래도 데이터가 많은 것 같아서 에어비앤비, 야놀자, 여기어때 등의 숙박업체 중 탑 2 (야놀자, 여기어때 점유율이 1, 2위라는 걸 처음 알았음)에서도 제주시, 게스트하우스로 줄여나갔다.

 

* 머릿속 생각은 숙박업체 등에서 좋은 평점, 또는 많이 방문하는 업체들의 리스트를 뽑는다. 어떤 점에서 방문을 하는지 주위 상권 분석을 한다. 추가로 있으면 좋을 서비스나 경쟁력 갖추기 위한 것들을 제시한다. 제주도뿐만 아니라 타 여행도시랑 비교해본다. 

 

 

오만했다. 처음 프로젝트면 욕심을 버리자

 

 

다음 편은 후기를 웹 스크래핑으로 뽑아오기 

+ Recent posts